From 148abf3ba57ca266173dd94893198c4b893c692d Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:26:00 -0500 Subject: [PATCH 01/49] Bump go-github from v78 to v79 in /scrape (#3828) --- scrape/apps.go | 2 +- scrape/apps_test.go | 2 +- scrape/go.mod | 2 +- scrape/go.sum | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scrape/apps.go b/scrape/apps.go index 17f835abedf..3a2e4bc3c52 100644 --- a/scrape/apps.go +++ b/scrape/apps.go @@ -18,7 +18,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" - "github.com/google/go-github/v78/github" + "github.com/google/go-github/v79/github" ) // AppRestrictionsEnabled returns whether the specified organization has diff --git a/scrape/apps_test.go b/scrape/apps_test.go index b0b447b20fc..19987ab4a0e 100644 --- a/scrape/apps_test.go +++ b/scrape/apps_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-github/v78/github" + "github.com/google/go-github/v79/github" ) func Test_AppRestrictionsEnabled(t *testing.T) { diff --git a/scrape/go.mod b/scrape/go.mod index 71fc6fe9a79..f83d55c73c8 100644 --- a/scrape/go.mod +++ b/scrape/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/PuerkitoBio/goquery v1.10.3 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v78 v78.0.0 + github.com/google/go-github/v79 v79.0.0 github.com/xlzd/gotp v0.1.0 golang.org/x/net v0.46.0 ) diff --git a/scrape/go.sum b/scrape/go.sum index f56684a4e91..fc04b824a26 100644 --- a/scrape/go.sum +++ b/scrape/go.sum @@ -6,8 +6,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v78 v78.0.0 h1:b1tytzFE8i//lRVDx5Qh/EdJbtTPtSVD3nF7hraEs9w= -github.com/google/go-github/v78 v78.0.0/go.mod h1:Uxvdzy82AkNlC6JQ57se9TqvmgBT7RF0ouHDNg2jd6g= +github.com/google/go-github/v79 v79.0.0 h1:MdodQojuFPBhmtwHiBcIGLw/e/wei2PvFX9ndxK0X4Y= +github.com/google/go-github/v79 v79.0.0/go.mod h1:OAFbNhq7fQwohojb06iIIQAB9CBGYLq999myfUFnrS4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= From 141f4f2755dddc8c031bb1ccd312a7f49502a3a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:17:28 -0500 Subject: [PATCH 02/49] build(deps): Bump github.com/PuerkitoBio/goquery from 1.10.3 to 1.11.0 in /scrape (#3833) --- scrape/go.mod | 4 ++-- scrape/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scrape/go.mod b/scrape/go.mod index f83d55c73c8..66343f26ba4 100644 --- a/scrape/go.mod +++ b/scrape/go.mod @@ -3,11 +3,11 @@ module github.com/google/go-github/scrape go 1.24.0 require ( - github.com/PuerkitoBio/goquery v1.10.3 + github.com/PuerkitoBio/goquery v1.11.0 github.com/google/go-cmp v0.7.0 github.com/google/go-github/v79 v79.0.0 github.com/xlzd/gotp v0.1.0 - golang.org/x/net v0.46.0 + golang.org/x/net v0.47.0 ) require ( diff --git a/scrape/go.sum b/scrape/go.sum index fc04b824a26..720a103822f 100644 --- a/scrape/go.sum +++ b/scrape/go.sum @@ -1,5 +1,5 @@ -github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= -github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -33,8 +33,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 90960cf858b9be1c0672c5803451795877f71832 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:45:16 -0500 Subject: [PATCH 03/49] build(deps): Bump actions/checkout from 5.0.0 to 5.0.1 in the actions group (#3834) --- .github/workflows/linter.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index dabfeb68c44..6159fbeed2c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -8,7 +8,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: 1.x diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index efaa56839cd..376b35b30a9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.go-version }} - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Get values for cache paths to be used in later steps - id: cache-paths From ea88bd7a6d7bbed1318f9bf6a133db9127ad363e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:48:29 -0500 Subject: [PATCH 04/49] build(deps): Bump golang.org/x/crypto from 0.43.0 to 0.44.0 in /example (#3835) --- example/go.mod | 14 +++++++------- example/go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/example/go.mod b/example/go.mod index 7a29510db2b..f0d519a7e85 100644 --- a/example/go.mod +++ b/example/go.mod @@ -9,8 +9,8 @@ require ( github.com/gofri/go-github-ratelimit/v2 v2.0.2 github.com/google/go-github/v79 v79.0.0 github.com/sigstore/sigstore-go v0.6.1 - golang.org/x/crypto v0.43.0 - golang.org/x/term v0.36.0 + golang.org/x/crypto v0.44.0 + golang.org/x/term v0.37.0 google.golang.org/appengine v1.6.8 ) @@ -87,11 +87,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.45.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/example/go.sum b/example/go.sum index b22570ebd34..ad8d8030b86 100644 --- a/example/go.sum +++ b/example/go.sum @@ -355,41 +355,41 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 45fa28c45ce175c05572d1f72b3d00abd7980216 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:53:26 -0500 Subject: [PATCH 05/49] build(deps): Bump github.com/alecthomas/kong from 1.12.1 to 1.13.0 in /tools (#3837) --- tools/go.mod | 2 +- tools/go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index f3bdf3adf5a..ecbed2b9ab5 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -3,7 +3,7 @@ module tools go 1.24.0 require ( - github.com/alecthomas/kong v1.12.1 + github.com/alecthomas/kong v1.13.0 github.com/getkin/kin-openapi v0.133.0 github.com/google/go-cmp v0.7.0 github.com/google/go-github/v79 v79.0.0 diff --git a/tools/go.sum b/tools/go.sum index 7a9e31bb4db..d543c89d8a6 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,9 +1,9 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.12.1 h1:iq6aMJDcFYP9uFrLdsiZQ2ZMmcshduyGv4Pek0MQPW0= -github.com/alecthomas/kong v1.12.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= +github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= From ea7dd818f63c868dcbd8bfe4a266161b719cc337 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Tue, 18 Nov 2025 23:52:05 +0530 Subject: [PATCH 06/49] feat: Add support for Enterprise GitHub App Installation APIs (#3830) --- github/enterprise_app_installation.go | 159 +++++++++++++++++ github/enterprise_app_installation_test.go | 197 +++++++++++++++++++++ github/github-accessors.go | 8 + github/github-accessors_test.go | 11 ++ 4 files changed, 375 insertions(+) create mode 100644 github/enterprise_app_installation.go create mode 100644 github/enterprise_app_installation_test.go diff --git a/github/enterprise_app_installation.go b/github/enterprise_app_installation.go new file mode 100644 index 00000000000..aed3ac753fd --- /dev/null +++ b/github/enterprise_app_installation.go @@ -0,0 +1,159 @@ +// 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" +) + +// InstallableOrganization represents an organization in an enterprise in which a GitHub app can be installed. +type InstallableOrganization struct { + ID int64 `json:"id"` + Login string `json:"login"` + AccessibleRepositoriesURL *string `json:"accessible_repositories_url,omitempty"` +} + +// AccessibleRepository represents a repository that can be made accessible to a GitHub app. +type AccessibleRepository struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` +} + +// InstallAppRequest represents the request to install a GitHub app on an enterprise-owned organization. +type InstallAppRequest struct { + // The Client ID of the GitHub App to install. + ClientID string `json:"client_id"` + // The selection of repositories that the GitHub app can access. + // Can be one of: all, selected, none + RepositorySelection string `json:"repository_selection"` + // A list of repository names that the GitHub App can access, if the repository_selection is set to selected. + Repositories []string `json:"repositories,omitempty"` +} + +// ListAppInstallableOrganizations lists the organizations in an enterprise that are installable for an app. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-enterprise-owned-organizations-that-can-have-github-apps-installed +// +//meta:operation GET /enterprises/{enterprise}/apps/installable_organizations +func (s *EnterpriseService) ListAppInstallableOrganizations(ctx context.Context, enterprise string, opts *ListOptions) ([]*InstallableOrganization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/installable_organizations", enterprise) + + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var orgs []*InstallableOrganization + resp, err := s.client.Do(ctx, req, &orgs) + if err != nil { + return nil, resp, err + } + + return orgs, resp, nil +} + +// ListAppAccessibleOrganizationRepositories lists the repositories accessible to an app in an enterprise-owned organization. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-repositories-belonging-to-an-enterprise-owned-organization +// +//meta:operation GET /enterprises/{enterprise}/apps/installable_organizations/{org}/accessible_repositories +func (s *EnterpriseService) ListAppAccessibleOrganizationRepositories(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*AccessibleRepository, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/installable_organizations/%v/accessible_repositories", enterprise, org) + + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var repos []*AccessibleRepository + resp, err := s.client.Do(ctx, req, &repos) + if err != nil { + return nil, resp, err + } + + return repos, resp, nil +} + +// ListAppInstallations lists the GitHub app installations associated with the given enterprise-owned organization. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#list-github-apps-installed-on-an-enterprise-owned-organization +// +//meta:operation GET /enterprises/{enterprise}/apps/organizations/{org}/installations +func (s *EnterpriseService) ListAppInstallations(ctx context.Context, enterprise, org string, opts *ListOptions) ([]*Installation, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org) + + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var installation []*Installation + resp, err := s.client.Do(ctx, req, &installation) + if err != nil { + return nil, resp, err + } + + return installation, resp, nil +} + +// InstallApp installs any valid GitHub app on the specified organization owned by the enterprise. +// If the app is already installed on the organization, and is suspended, it will be unsuspended. If the app has a pending installation request, they will all be approved. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#install-a-github-app-on-an-enterprise-owned-organization +// +//meta:operation POST /enterprises/{enterprise}/apps/organizations/{org}/installations +func (s *EnterpriseService) InstallApp(ctx context.Context, enterprise, org string, request InstallAppRequest) (*Installation, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations", enterprise, org) + req, err := s.client.NewRequest("POST", u, request) + if err != nil { + return nil, nil, err + } + + var installation *Installation + resp, err := s.client.Do(ctx, req, &installation) + if err != nil { + return nil, resp, err + } + + return installation, resp, nil +} + +// UninstallApp uninstalls a GitHub app from an organization. Any app installed on the organization can be removed. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#uninstall-a-github-app-from-an-enterprise-owned-organization +// +//meta:operation DELETE /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id} +func (s *EnterpriseService) UninstallApp(ctx context.Context, enterprise, org string, installationID int64) (*Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v", enterprise, org, installationID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/enterprise_app_installation_test.go b/github/enterprise_app_installation_test.go new file mode 100644 index 00000000000..766982a28d4 --- /dev/null +++ b/github/enterprise_app_installation_test.go @@ -0,0 +1,197 @@ +// 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 ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEnterpriseService_ListAppInstallableOrganizations(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/installable_organizations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1, "login":"org1"}]`) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + got, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", opts) + if err != nil { + t.Fatalf("Enterprise.ListAppInstallableOrganizations returned error: %v", err) + } + + want := []*InstallableOrganization{ + {ID: int64(1), Login: "org1"}, + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.ListAppInstallableOrganizations = %+v, want %+v", got, want) + } + + const methodName = "ListAppInstallableOrganizations" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "\n", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListAppInstallableOrganizations(ctx, "e", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_ListAppAccessibleOrganizationRepositories(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/installable_organizations/org1/accessible_repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":10, "name":"repo1", "full_name":"org1/repo1"}]`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := t.Context() + repos, _, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", opts) + if err != nil { + t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned error: %v", err) + } + + want := []*AccessibleRepository{ + {ID: int64(10), Name: "repo1", FullName: "org1/repo1"}, + } + + if !cmp.Equal(repos, want) { + t.Errorf("Enterprise.ListAppAccessibleOrganizationRepositories returned %+v, want %+v", repos, want) + } + + const methodName = "ListAppAccessibleOrganizationRepositories" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "\n", "org1", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListAppAccessibleOrganizationRepositories(ctx, "e", "org1", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_ListAppInstallations(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `[{"id":99}]`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := t.Context() + installations, _, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", opts) + if err != nil { + t.Errorf("ListAppInstallations returned error: %v", err) + } + want := []*Installation{ + {ID: Ptr(int64(99))}, + } + + if !cmp.Equal(installations, want) { + t.Errorf("ListAppInstallations returned %+v, want %+v", installations, want) + } + + const methodName = "ListAppInstallations" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ListAppInstallations(ctx, "\n", "org1", &ListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListAppInstallations(ctx, "e", "org1", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_InstallApp(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"client_id":"cid","repository_selection":"selected","repositories":["r1","r2"]}`+"\n") + fmt.Fprint(w, `{"id":555}`) + }) + + req := InstallAppRequest{ + ClientID: "cid", + RepositorySelection: "selected", + Repositories: []string{"r1", "r2"}, + } + + ctx := t.Context() + installation, _, err := client.Enterprise.InstallApp(ctx, "e", "org1", req) + if err != nil { + t.Errorf("InstallApp returned error: %v", err) + } + + want := &Installation{ID: Ptr(int64(555))} + + if !cmp.Equal(installation, want) { + t.Errorf("InstallApp returned %+v, want %+v", installation, want) + } + + const methodName = "InstallApp" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.InstallApp(ctx, "e", "org1", req) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_UninstallApp(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/organizations/org1/installations/123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Enterprise.UninstallApp(ctx, "e", "org1", 123) + if err != nil { + t.Errorf("UninstallApp returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("UninstallApp returned status %v, want %v", resp.StatusCode, http.StatusNoContent) + } + + const methodName = "UninstallApp" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.UninstallApp(ctx, "e", "org1", 123) + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 2a1a9394f67..d9ed060d5ce 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -11286,6 +11286,14 @@ func (i *Import) GetVCSUsername() string { return *i.VCSUsername } +// GetAccessibleRepositoriesURL returns the AccessibleRepositoriesURL field if it's non-nil, zero value otherwise. +func (i *InstallableOrganization) GetAccessibleRepositoriesURL() string { + if i == nil || i.AccessibleRepositoriesURL == nil { + return "" + } + return *i.AccessibleRepositoriesURL +} + // GetAccessTokensURL returns the AccessTokensURL field if it's non-nil, zero value otherwise. func (i *Installation) GetAccessTokensURL() string { if i == nil || i.AccessTokensURL == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index c926697944f..d2ae9e7b911 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -14662,6 +14662,17 @@ func TestImport_GetVCSUsername(tt *testing.T) { i.GetVCSUsername() } +func TestInstallableOrganization_GetAccessibleRepositoriesURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &InstallableOrganization{AccessibleRepositoriesURL: &zeroValue} + i.GetAccessibleRepositoriesURL() + i = &InstallableOrganization{} + i.GetAccessibleRepositoriesURL() + i = nil + i.GetAccessibleRepositoriesURL() +} + func TestInstallation_GetAccessTokensURL(tt *testing.T) { tt.Parallel() var zeroValue string From ae3fff0df782aa757a01ea1737f66f436dee2f4d Mon Sep 17 00:00:00 2001 From: Svyatoslav Pidgorny <25411814+SP3269@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:19:04 +1100 Subject: [PATCH 07/49] Add ParentIssueURL field to Issue struct (#3841) --- github/github-accessors.go | 8 ++++++++ github/github-accessors_test.go | 11 +++++++++++ github/github-stringify_test.go | 3 ++- github/issues.go | 1 + github/issues_test.go | 2 ++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index d9ed060d5ce..52798ac89d3 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -12558,6 +12558,14 @@ func (i *Issue) GetNumber() int { return *i.Number } +// GetParentIssueURL returns the ParentIssueURL field if it's non-nil, zero value otherwise. +func (i *Issue) GetParentIssueURL() string { + if i == nil || i.ParentIssueURL == nil { + return "" + } + return *i.ParentIssueURL +} + // GetPullRequestLinks returns the PullRequestLinks field. func (i *Issue) GetPullRequestLinks() *PullRequestLinks { if i == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index d2ae9e7b911..894a3685b95 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -16327,6 +16327,17 @@ func TestIssue_GetNumber(tt *testing.T) { i.GetNumber() } +func TestIssue_GetParentIssueURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &Issue{ParentIssueURL: &zeroValue} + i.GetParentIssueURL() + i = &Issue{} + i.GetParentIssueURL() + i = nil + i.GetParentIssueURL() +} + func TestIssue_GetPullRequestLinks(tt *testing.T) { tt.Parallel() i := &Issue{} diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index 6f0cdbeb8f1..5bf246a265d 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -931,6 +931,7 @@ func TestIssue_String(t *testing.T) { EventsURL: Ptr(""), LabelsURL: Ptr(""), RepositoryURL: Ptr(""), + ParentIssueURL: Ptr(""), Milestone: &Milestone{}, PullRequestLinks: &PullRequestLinks{}, Repository: &Repository{}, @@ -940,7 +941,7 @@ func TestIssue_String(t *testing.T) { Type: &IssueType{}, ActiveLockReason: Ptr(""), } - want := `github.Issue{ID:0, Number:0, State:"", StateReason:"", Locked:false, Title:"", Body:"", AuthorAssociation:"", User:github.User{}, Assignee:github.User{}, Comments:0, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, ClosedBy:github.User{}, URL:"", HTMLURL:"", CommentsURL:"", EventsURL:"", LabelsURL:"", RepositoryURL:"", Milestone:github.Milestone{}, PullRequestLinks:github.PullRequestLinks{}, Repository:github.Repository{}, Reactions:github.Reactions{}, NodeID:"", Draft:false, Type:github.IssueType{}, ActiveLockReason:""}` + want := `github.Issue{ID:0, Number:0, State:"", StateReason:"", Locked:false, Title:"", Body:"", AuthorAssociation:"", User:github.User{}, Assignee:github.User{}, Comments:0, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, ClosedBy:github.User{}, URL:"", HTMLURL:"", CommentsURL:"", EventsURL:"", LabelsURL:"", RepositoryURL:"", ParentIssueURL:"", Milestone:github.Milestone{}, PullRequestLinks:github.PullRequestLinks{}, Repository:github.Repository{}, Reactions:github.Reactions{}, NodeID:"", Draft:false, Type:github.IssueType{}, ActiveLockReason:""}` if got := v.String(); got != want { t.Errorf("Issue.String = %v, want %v", got, want) } diff --git a/github/issues.go b/github/issues.go index 72d0dbae6bd..d19987f26bf 100644 --- a/github/issues.go +++ b/github/issues.go @@ -54,6 +54,7 @@ type Issue struct { EventsURL *string `json:"events_url,omitempty"` LabelsURL *string `json:"labels_url,omitempty"` RepositoryURL *string `json:"repository_url,omitempty"` + ParentIssueURL *string `json:"parent_issue_url,omitempty"` Milestone *Milestone `json:"milestone,omitempty"` PullRequestLinks *PullRequestLinks `json:"pull_request,omitempty"` Repository *Repository `json:"repository,omitempty"` diff --git a/github/issues_test.go b/github/issues_test.go index a859fd7c9ec..53fe46aa81d 100644 --- a/github/issues_test.go +++ b/github/issues_test.go @@ -586,6 +586,7 @@ func TestIssue_Marshal(t *testing.T) { EventsURL: Ptr("eurl"), LabelsURL: Ptr("lurl"), RepositoryURL: Ptr("rurl"), + ParentIssueURL: Ptr("piurl"), Milestone: &Milestone{ID: Ptr(int64(1))}, PullRequestLinks: &PullRequestLinks{URL: Ptr("url")}, Repository: &Repository{ID: Ptr(int64(1))}, @@ -629,6 +630,7 @@ func TestIssue_Marshal(t *testing.T) { "events_url": "eurl", "labels_url": "lurl", "repository_url": "rurl", + "parent_issue_url": "piurl", "milestone": { "id": 1 }, From 903265bfcfe4c499bec1973f7075b8a60503c19b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:54:51 -0500 Subject: [PATCH 08/49] build(deps): Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in /example (#3842) --- example/go.mod | 4 ++-- example/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/go.mod b/example/go.mod index f0d519a7e85..96ac958d2a8 100644 --- a/example/go.mod +++ b/example/go.mod @@ -9,7 +9,7 @@ require ( github.com/gofri/go-github-ratelimit/v2 v2.0.2 github.com/google/go-github/v79 v79.0.0 github.com/sigstore/sigstore-go v0.6.1 - golang.org/x/crypto v0.44.0 + golang.org/x/crypto v0.45.0 golang.org/x/term v0.37.0 google.golang.org/appengine v1.6.8 ) @@ -88,7 +88,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/example/go.sum b/example/go.sum index ad8d8030b86..f14290e4637 100644 --- a/example/go.sum +++ b/example/go.sum @@ -355,8 +355,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -365,8 +365,8 @@ golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 4bdf8fb308d1140cb3276d193c2c882263d40119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:47:21 -0500 Subject: [PATCH 09/49] build(deps): Bump the actions group with 2 updates (#3844) --- .github/workflows/linter.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 6159fbeed2c..63167ade312 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -8,8 +8,8 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: 1.x cache-dependency-path: "**/go.sum" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 376b35b30a9..c612df5ee44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,10 +42,10 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ matrix.go-version }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Get values for cache paths to be used in later steps - id: cache-paths From 55ef8596d78daff54b2b9ca2fcd87c39d72edb27 Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:13:07 +0100 Subject: [PATCH 10/49] feat!: Implement Enterprise SCIM - EnterpriseService.ListProvisionedSCIMUsers (#3839) BREAKING CHANGE: `ListProvisionedSCIMGroupsEnterpriseOptions` optional fields are now pointers. --- github/enterprise_scim.go | 104 ++++++++++++- github/enterprise_scim_test.go | 251 +++++++++++++++++++++++++++++++- github/github-accessors.go | 144 ++++++++++++++++++ github/github-accessors_test.go | 192 ++++++++++++++++++++++++ github/github.go | 1 + 5 files changed, 682 insertions(+), 10 deletions(-) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index fe7aa3406df..806954360a0 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -14,6 +14,10 @@ import ( // This constant represents the standard SCIM core schema for group objects as defined by RFC 7643. const SCIMSchemasURINamespacesGroups = "urn:ietf:params:scim:schemas:core:2.0:Group" +// SCIMSchemasURINamespacesUser is the SCIM schema URI namespace for user resources. +// This constant represents the standard SCIM core schema for user objects as defined by RFC 7643. +const SCIMSchemasURINamespacesUser = "urn:ietf:params:scim:schemas:core:2.0:User" + // SCIMSchemasURINamespacesListResponse is the SCIM schema URI namespace for list response resources. // This constant represents the standard SCIM namespace for list responses used in paginated queries, as defined by RFC 7644. const SCIMSchemasURINamespacesListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse" @@ -61,15 +65,79 @@ type SCIMEnterpriseGroups struct { type ListProvisionedSCIMGroupsEnterpriseOptions struct { // If specified, only results that match the specified filter will be returned. // Possible filters are `externalId`, `id`, and `displayName`. For example, `externalId eq "a123"`. - Filter string `url:"filter,omitempty"` + Filter *string `url:"filter,omitempty"` // Excludes the specified attribute from being returned in the results. - ExcludedAttributes string `url:"excludedAttributes,omitempty"` + ExcludedAttributes *string `url:"excludedAttributes,omitempty"` + // Used for pagination: the starting index of the first result to return when paginating through values. + // Default: 1. + StartIndex *int `url:"startIndex,omitempty"` + // Used for pagination: the number of results to return per page. + // Default: 30. + Count *int `url:"count,omitempty"` +} + +// SCIMEnterpriseUserAttributes represents supported SCIM enterprise user attributes. +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-user-attributes +type SCIMEnterpriseUserAttributes struct { + DisplayName string `json:"displayName"` // Human-readable name for a user + Name *SCIMEnterpriseUserName `json:"name,omitempty"` // The user's full name + UserName string `json:"userName"` // The username for the user (GitHub Account after normalized), generated by the SCIM provider. Must be unique per user. + Emails []*SCIMEnterpriseUserEmail `json:"emails"` // List of the user's emails. They all must be unique per user. + Roles []*SCIMEnterpriseUserRole `json:"roles,omitempty"` // List of the user's roles. + ExternalID string `json:"externalId"` // This identifier is generated by a SCIM provider. Must be unique per user. + Active bool `json:"active"` // Indicates whether the identity is active (true) or should be suspended (false). + Schemas []string `json:"schemas"` // The URIs that are used to indicate the namespaces of the SCIM schemas. + // Bellow: Only populated as a result of calling SetSCIMInformationForProvisionedUser: + ID *string `json:"id,omitempty"` // Identifier generated by the GitHub's SCIM endpoint. + Groups []*SCIMEnterpriseDisplayReference `json:"groups,omitempty"` // List of groups who are assigned to the user in SCIM provider + Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the user. +} + +// SCIMEnterpriseUserName represents SCIM enterprise user's name information. +type SCIMEnterpriseUserName struct { + GivenName string `json:"givenName"` // The first name of the user. + FamilyName string `json:"familyName"` // The last name of the user. + Formatted *string `json:"formatted,omitempty"` // The user's full name, including all middle names, titles, and suffixes, formatted for display. + MiddleName *string `json:"middleName,omitempty"` // The middle name(s) of the user. +} + +// SCIMEnterpriseUserEmail represents SCIM enterprise user's emails. +type SCIMEnterpriseUserEmail struct { + Value string `json:"value"` // The email address. + Primary bool `json:"primary"` // Whether this email address is the primary address. + Type string `json:"type"` // The type of email address +} + +// SCIMEnterpriseUserRole is an enterprise-wide role granted to the user. +type SCIMEnterpriseUserRole struct { + Value string `json:"value"` // The role value representing a user role in GitHub. + Display *string `json:"display,omitempty"` + Type *string `json:"type,omitempty"` + Primary *bool `json:"primary,omitempty"` // Is the role a primary role for the user? +} + +// SCIMEnterpriseUsers represents the result of calling ProvisionSCIMEnterpriseUser. +type SCIMEnterpriseUsers struct { + Schemas []string `json:"schemas,omitempty"` + TotalResults *int `json:"totalResults,omitempty"` + ItemsPerPage *int `json:"itemsPerPage,omitempty"` + StartIndex *int `json:"startIndex,omitempty"` + Resources []*SCIMEnterpriseUserAttributes `json:"Resources,omitempty"` +} + +// ListProvisionedSCIMUsersEnterpriseOptions represents query parameters for ListSCIMProvisionedUsers. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise +type ListProvisionedSCIMUsersEnterpriseOptions struct { + // If specified, only results that match the specified filter will be returned. + // Possible filters are `userName`, `externalId`, `id`, and `displayName`. For example, `externalId eq "a123"`. + Filter *string `url:"filter,omitempty"` // Used for pagination: the starting index of the first result to return when paginating through values. // Default: 1. - StartIndex int `url:"startIndex,omitempty"` + StartIndex *int `url:"startIndex,omitempty"` // Used for pagination: the number of results to return per page. // Default: 30. - Count int `url:"count,omitempty"` + Count *int `url:"count,omitempty"` } // ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise. @@ -88,6 +156,7 @@ func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enter if err != nil { return nil, nil, err } + req.Header.Set("Accept", mediaTypeSCIM) groups := new(SCIMEnterpriseGroups) resp, err := s.client.Do(ctx, req, groups) @@ -97,3 +166,30 @@ func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enter return groups, resp, nil } + +// ListProvisionedSCIMUsers lists provisioned SCIM enterprise users. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise +// +//meta:operation GET /scim/v2/enterprises/{enterprise}/Users +func (s *EnterpriseService) ListProvisionedSCIMUsers(ctx context.Context, enterprise string, opts *ListProvisionedSCIMUsersEnterpriseOptions) (*SCIMEnterpriseUsers, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users", enterprise) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + users := new(SCIMEnterpriseUsers) + resp, err := s.client.Do(ctx, req, users) + if err != nil { + return nil, resp, err + } + + return users, resp, nil +} diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index 5b1d222ae15..adf9099ccea 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -67,6 +67,139 @@ func TestSCIMEnterpriseGroups_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } +func TestSCIMEnterpriseUsers_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &SCIMEnterpriseUsers{}, "{}") + + u := &SCIMEnterpriseUsers{ + Schemas: []string{SCIMSchemasURINamespacesListResponse}, + TotalResults: Ptr(1), + ItemsPerPage: Ptr(1), + StartIndex: Ptr(1), + Resources: []*SCIMEnterpriseUserAttributes{{ + Active: true, + Emails: []*SCIMEnterpriseUserEmail{{ + Primary: true, + Type: "work", + Value: "un1@email.com", + }}, + Roles: []*SCIMEnterpriseUserRole{{ + Display: Ptr("rd1"), + Primary: Ptr(true), + Type: Ptr("rt1"), + Value: "rv1", + }}, + Schemas: []string{SCIMSchemasURINamespacesUser}, + UserName: "un1", + Groups: []*SCIMEnterpriseDisplayReference{{ + Value: "idgn1", + Ref: "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1", + Display: Ptr("gn1"), + }}, + ID: Ptr("idun1"), + ExternalID: "eidun1", + DisplayName: "dun1", + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/User/idun1"), + }, + Name: &SCIMEnterpriseUserName{ + GivenName: "gnn1", + FamilyName: "fnn1", + Formatted: Ptr("f1"), + MiddleName: Ptr("mn1"), + }, + }}, + } + + want := `{ + "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], + "TotalResults": 1, + "itemsPerPage": 1, + "StartIndex": 1, + "Resources": [{ + "active": true, + "emails": [{ + "primary": true, + "type": "work", + "value": "un1@email.com" + }], + "roles": [{ + "display": "rd1", + "primary": true, + "type": "rt1", + "value": "rv1" + }], + "schemas": ["` + SCIMSchemasURINamespacesUser + `"], + "userName": "un1", + "groups": [{ + "value": "idgn1", + "$ref": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1", + "display": "gn1" + }], + "id": "idun1", + "externalId": "eidun1", + "name": { + "givenName": "gnn1", + "familyName": "fnn1", + "formatted": "f1", + "middleName": "mn1" + }, + "displayName": "dun1", + "meta": { + "resourceType": "User", + "created": ` + referenceTimeStr + `, + "lastModified": ` + referenceTimeStr + `, + "location": "https://api.github.com/scim/v2/enterprises/ee/User/idun1" + } + }] + }` + + testJSONMarshal(t, u, want) +} + +func TestListProvisionedSCIMGroupsEnterpriseOptions_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ListProvisionedSCIMGroupsEnterpriseOptions{}, "{}") + + u := &ListProvisionedSCIMGroupsEnterpriseOptions{ + Filter: Ptr("f"), + ExcludedAttributes: Ptr("ea"), + StartIndex: Ptr(5), + Count: Ptr(9), + } + + want := `{ + "filter": "f", + "excludedAttributes": "ea", + "startIndex": 5, + "count": 9 + }` + + testJSONMarshal(t, u, want) +} + +func TestListProvisionedSCIMUsersEnterpriseOptions_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ListProvisionedSCIMUsersEnterpriseOptions{}, "{}") + + u := &ListProvisionedSCIMUsersEnterpriseOptions{ + Filter: Ptr("f"), + StartIndex: Ptr(3), + Count: Ptr(7), + } + + want := `{ + "filter": "f", + "startIndex": 3, + "count": 7 + }` + + testJSONMarshal(t, u, want) +} + func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) { t.Parallel() testJSONMarshal(t, &SCIMEnterpriseGroupAttributes{}, "{}") @@ -110,12 +243,13 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } -func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) { +func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/scim/v2/enterprises/ee/Groups", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeSCIM) testFormValues(t, r, values{ "startIndex": "1", "excludedAttributes": "members,meta", @@ -150,14 +284,14 @@ func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) { ctx := t.Context() opts := &ListProvisionedSCIMGroupsEnterpriseOptions{ - StartIndex: 1, - ExcludedAttributes: "members,meta", - Count: 3, - Filter: `externalId eq "914a"`, + StartIndex: Ptr(1), + ExcludedAttributes: Ptr("members,meta"), + Count: Ptr(3), + Filter: Ptr(`externalId eq "914a"`), } groups, _, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts) if err != nil { - t.Errorf("Enterprise.ListProvisionedSCIMGroups returned error: %v", err) + t.Fatalf("Enterprise.ListProvisionedSCIMGroups returned unexpected error: %v", err) } want := SCIMEnterpriseGroups{ @@ -199,3 +333,108 @@ func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) { return r, err }) } + +func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/octo-org/Users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeSCIM) + testFormValues(t, r, values{ + "startIndex": "1", + "count": "3", + "filter": `userName eq "octocat@github.com"`, + }) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], + "totalResults": 1, + "itemsPerPage": 1, + "startIndex": 1, + "Resources": [ + { + "schemas": ["` + SCIMSchemasURINamespacesUser + `"], + "id": "5fc0", + "externalId": "00u1", + "userName": "octocat@github.com", + "displayName": "Mona Octocat", + "name": { + "givenName": "Mona", + "familyName": "Octocat", + "formatted": "Mona Octocat" + }, + "emails": [ + { + "value": "octocat@github.com", + "primary": true + } + ], + "active": true, + "meta": { + "resourceType": "User", + "created": ` + referenceTimeStr + `, + "lastModified": ` + referenceTimeStr + `, + "location": "https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0" + } + } + ] + }`)) + }) + + ctx := t.Context() + opts := &ListProvisionedSCIMUsersEnterpriseOptions{ + StartIndex: Ptr(1), + Count: Ptr(3), + Filter: Ptr(`userName eq "octocat@github.com"`), + } + users, _, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "octo-org", opts) + if err != nil { + t.Fatalf("Enterprise.ListProvisionedSCIMUsers returned unexpected error: %v", err) + } + + want := SCIMEnterpriseUsers{ + Schemas: []string{SCIMSchemasURINamespacesListResponse}, + TotalResults: Ptr(1), + ItemsPerPage: Ptr(1), + StartIndex: Ptr(1), + Resources: []*SCIMEnterpriseUserAttributes{{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ID: Ptr("5fc0"), + ExternalID: "00u1", + UserName: "octocat@github.com", + DisplayName: "Mona Octocat", + Name: &SCIMEnterpriseUserName{ + GivenName: "Mona", + FamilyName: "Octocat", + Formatted: Ptr("Mona Octocat"), + }, + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "octocat@github.com", + Primary: true, + }}, + Active: true, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0"), + }, + }}, + } + + if diff := cmp.Diff(want, *users); diff != "" { + t.Errorf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "ListProvisionedSCIMUsers" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ListProvisionedSCIMUsers(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, r, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "o", opts) + return r, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 52798ac89d3..b95be28e733 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -14350,6 +14350,62 @@ func (l *ListProjectsPaginationOptions) GetPerPage() int { return *l.PerPage } +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMGroupsEnterpriseOptions) GetCount() int { + if l == nil || l.Count == nil { + return 0 + } + return *l.Count +} + +// GetExcludedAttributes returns the ExcludedAttributes field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMGroupsEnterpriseOptions) GetExcludedAttributes() string { + if l == nil || l.ExcludedAttributes == nil { + return "" + } + return *l.ExcludedAttributes +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMGroupsEnterpriseOptions) GetFilter() string { + if l == nil || l.Filter == nil { + return "" + } + return *l.Filter +} + +// GetStartIndex returns the StartIndex field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMGroupsEnterpriseOptions) GetStartIndex() int { + if l == nil || l.StartIndex == nil { + return 0 + } + return *l.StartIndex +} + +// GetCount returns the Count field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMUsersEnterpriseOptions) GetCount() int { + if l == nil || l.Count == nil { + return 0 + } + return *l.Count +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMUsersEnterpriseOptions) GetFilter() string { + if l == nil || l.Filter == nil { + return "" + } + return *l.Filter +} + +// GetStartIndex returns the StartIndex field if it's non-nil, zero value otherwise. +func (l *ListProvisionedSCIMUsersEnterpriseOptions) GetStartIndex() int { + if l == nil || l.StartIndex == nil { + return 0 + } + return *l.StartIndex +} + // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. func (l *ListRepositories) GetTotalCount() int { if l == nil || l.TotalCount == nil { @@ -26262,6 +26318,94 @@ func (s *SCIMEnterpriseMeta) GetLocation() string { return *s.Location } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserAttributes) GetID() string { + if s == nil || s.ID == nil { + return "" + } + return *s.ID +} + +// GetMeta returns the Meta field. +func (s *SCIMEnterpriseUserAttributes) GetMeta() *SCIMEnterpriseMeta { + if s == nil { + return nil + } + return s.Meta +} + +// GetName returns the Name field. +func (s *SCIMEnterpriseUserAttributes) GetName() *SCIMEnterpriseUserName { + if s == nil { + return nil + } + return s.Name +} + +// GetFormatted returns the Formatted field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserName) GetFormatted() string { + if s == nil || s.Formatted == nil { + return "" + } + return *s.Formatted +} + +// GetMiddleName returns the MiddleName field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserName) GetMiddleName() string { + if s == nil || s.MiddleName == nil { + return "" + } + return *s.MiddleName +} + +// GetDisplay returns the Display field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserRole) GetDisplay() string { + if s == nil || s.Display == nil { + return "" + } + return *s.Display +} + +// GetPrimary returns the Primary field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserRole) GetPrimary() bool { + if s == nil || s.Primary == nil { + return false + } + return *s.Primary +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUserRole) GetType() string { + if s == nil || s.Type == nil { + return "" + } + return *s.Type +} + +// GetItemsPerPage returns the ItemsPerPage field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUsers) GetItemsPerPage() int { + if s == nil || s.ItemsPerPage == nil { + return 0 + } + return *s.ItemsPerPage +} + +// GetStartIndex returns the StartIndex field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUsers) GetStartIndex() int { + if s == nil || s.StartIndex == nil { + return 0 + } + return *s.StartIndex +} + +// GetTotalResults returns the TotalResults field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseUsers) GetTotalResults() int { + if s == nil || s.TotalResults == nil { + return 0 + } + return *s.TotalResults +} + // GetCreated returns the Created field if it's non-nil, zero value otherwise. func (s *SCIMMeta) GetCreated() Timestamp { if s == nil || s.Created == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 894a3685b95..9abcb49dcde 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -18656,6 +18656,83 @@ func TestListProjectsPaginationOptions_GetPerPage(tt *testing.T) { l.GetPerPage() } +func TestListProvisionedSCIMGroupsEnterpriseOptions_GetCount(tt *testing.T) { + tt.Parallel() + var zeroValue int + l := &ListProvisionedSCIMGroupsEnterpriseOptions{Count: &zeroValue} + l.GetCount() + l = &ListProvisionedSCIMGroupsEnterpriseOptions{} + l.GetCount() + l = nil + l.GetCount() +} + +func TestListProvisionedSCIMGroupsEnterpriseOptions_GetExcludedAttributes(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProvisionedSCIMGroupsEnterpriseOptions{ExcludedAttributes: &zeroValue} + l.GetExcludedAttributes() + l = &ListProvisionedSCIMGroupsEnterpriseOptions{} + l.GetExcludedAttributes() + l = nil + l.GetExcludedAttributes() +} + +func TestListProvisionedSCIMGroupsEnterpriseOptions_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProvisionedSCIMGroupsEnterpriseOptions{Filter: &zeroValue} + l.GetFilter() + l = &ListProvisionedSCIMGroupsEnterpriseOptions{} + l.GetFilter() + l = nil + l.GetFilter() +} + +func TestListProvisionedSCIMGroupsEnterpriseOptions_GetStartIndex(tt *testing.T) { + tt.Parallel() + var zeroValue int + l := &ListProvisionedSCIMGroupsEnterpriseOptions{StartIndex: &zeroValue} + l.GetStartIndex() + l = &ListProvisionedSCIMGroupsEnterpriseOptions{} + l.GetStartIndex() + l = nil + l.GetStartIndex() +} + +func TestListProvisionedSCIMUsersEnterpriseOptions_GetCount(tt *testing.T) { + tt.Parallel() + var zeroValue int + l := &ListProvisionedSCIMUsersEnterpriseOptions{Count: &zeroValue} + l.GetCount() + l = &ListProvisionedSCIMUsersEnterpriseOptions{} + l.GetCount() + l = nil + l.GetCount() +} + +func TestListProvisionedSCIMUsersEnterpriseOptions_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProvisionedSCIMUsersEnterpriseOptions{Filter: &zeroValue} + l.GetFilter() + l = &ListProvisionedSCIMUsersEnterpriseOptions{} + l.GetFilter() + l = nil + l.GetFilter() +} + +func TestListProvisionedSCIMUsersEnterpriseOptions_GetStartIndex(tt *testing.T) { + tt.Parallel() + var zeroValue int + l := &ListProvisionedSCIMUsersEnterpriseOptions{StartIndex: &zeroValue} + l.GetStartIndex() + l = &ListProvisionedSCIMUsersEnterpriseOptions{} + l.GetStartIndex() + l = nil + l.GetStartIndex() +} + func TestListRepositories_GetTotalCount(tt *testing.T) { tt.Parallel() var zeroValue int @@ -33889,6 +33966,121 @@ func TestSCIMEnterpriseMeta_GetLocation(tt *testing.T) { s.GetLocation() } +func TestSCIMEnterpriseUserAttributes_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseUserAttributes{ID: &zeroValue} + s.GetID() + s = &SCIMEnterpriseUserAttributes{} + s.GetID() + s = nil + s.GetID() +} + +func TestSCIMEnterpriseUserAttributes_GetMeta(tt *testing.T) { + tt.Parallel() + s := &SCIMEnterpriseUserAttributes{} + s.GetMeta() + s = nil + s.GetMeta() +} + +func TestSCIMEnterpriseUserAttributes_GetName(tt *testing.T) { + tt.Parallel() + s := &SCIMEnterpriseUserAttributes{} + s.GetName() + s = nil + s.GetName() +} + +func TestSCIMEnterpriseUserName_GetFormatted(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseUserName{Formatted: &zeroValue} + s.GetFormatted() + s = &SCIMEnterpriseUserName{} + s.GetFormatted() + s = nil + s.GetFormatted() +} + +func TestSCIMEnterpriseUserName_GetMiddleName(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseUserName{MiddleName: &zeroValue} + s.GetMiddleName() + s = &SCIMEnterpriseUserName{} + s.GetMiddleName() + s = nil + s.GetMiddleName() +} + +func TestSCIMEnterpriseUserRole_GetDisplay(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseUserRole{Display: &zeroValue} + s.GetDisplay() + s = &SCIMEnterpriseUserRole{} + s.GetDisplay() + s = nil + s.GetDisplay() +} + +func TestSCIMEnterpriseUserRole_GetPrimary(tt *testing.T) { + tt.Parallel() + var zeroValue bool + s := &SCIMEnterpriseUserRole{Primary: &zeroValue} + s.GetPrimary() + s = &SCIMEnterpriseUserRole{} + s.GetPrimary() + s = nil + s.GetPrimary() +} + +func TestSCIMEnterpriseUserRole_GetType(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseUserRole{Type: &zeroValue} + s.GetType() + s = &SCIMEnterpriseUserRole{} + s.GetType() + s = nil + s.GetType() +} + +func TestSCIMEnterpriseUsers_GetItemsPerPage(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SCIMEnterpriseUsers{ItemsPerPage: &zeroValue} + s.GetItemsPerPage() + s = &SCIMEnterpriseUsers{} + s.GetItemsPerPage() + s = nil + s.GetItemsPerPage() +} + +func TestSCIMEnterpriseUsers_GetStartIndex(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SCIMEnterpriseUsers{StartIndex: &zeroValue} + s.GetStartIndex() + s = &SCIMEnterpriseUsers{} + s.GetStartIndex() + s = nil + s.GetStartIndex() +} + +func TestSCIMEnterpriseUsers_GetTotalResults(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SCIMEnterpriseUsers{TotalResults: &zeroValue} + s.GetTotalResults() + s = &SCIMEnterpriseUsers{} + s.GetTotalResults() + s = nil + s.GetTotalResults() +} + func TestSCIMMeta_GetCreated(tt *testing.T) { tt.Parallel() var zeroValue Timestamp diff --git a/github/github.go b/github/github.go index 8e7d58ae671..eba55176108 100644 --- a/github/github.go +++ b/github/github.go @@ -55,6 +55,7 @@ const ( mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json" mediaTypeStarring = "application/vnd.github.star+json" + mediaTypeSCIM = "application/scim+json" // Media Type values to access preview APIs. // These media types will be added to the API request as headers From 3afe183d384c353f62195abad00d39b5426ec121 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 26 Nov 2025 05:56:00 +0200 Subject: [PATCH 11/49] Add custom `structfield` linter to check struct field names and tags (#3843) --- .custom-gcl.yml | 4 +- .golangci.yml | 218 ++++++++++- tools/jsonfieldname/jsonfieldname.go | 234 ----------- .../testdata/src/has-warnings/main.go | 14 - .../testdata/src/no-warnings/main.go | 13 - tools/{jsonfieldname => structfield}/go.mod | 2 +- tools/{jsonfieldname => structfield}/go.sum | 0 tools/structfield/structfield.go | 362 ++++++++++++++++++ .../structfield_test.go} | 13 +- .../testdata/src/has-warnings/main.go | 37 ++ .../testdata/src/no-warnings/main.go | 46 +++ 11 files changed, 668 insertions(+), 275 deletions(-) delete mode 100644 tools/jsonfieldname/jsonfieldname.go delete mode 100644 tools/jsonfieldname/testdata/src/has-warnings/main.go delete mode 100644 tools/jsonfieldname/testdata/src/no-warnings/main.go rename tools/{jsonfieldname => structfield}/go.mod (87%) rename tools/{jsonfieldname => structfield}/go.sum (100%) create mode 100644 tools/structfield/structfield.go rename tools/{jsonfieldname/jsonfieldname_test.go => structfield/structfield_test.go} (64%) create mode 100644 tools/structfield/testdata/src/has-warnings/main.go create mode 100644 tools/structfield/testdata/src/no-warnings/main.go diff --git a/.custom-gcl.yml b/.custom-gcl.yml index 772345c1cbd..a0a0945e11a 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -2,7 +2,7 @@ version: v2.6.1 plugins: - module: "github.com/google/go-github/v79/tools/fmtpercentv" path: ./tools/fmtpercentv - - module: "github.com/google/go-github/v79/tools/jsonfieldname" - path: ./tools/jsonfieldname - module: "github.com/google/go-github/v79/tools/sliceofpointers" path: ./tools/sliceofpointers + - module: "github.com/google/go-github/v79/tools/structfield" + path: ./tools/structfield diff --git a/.golangci.yml b/.golangci.yml index 139a4a71188..df503f41577 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,7 +16,6 @@ linters: - goheader - gosec - intrange - - jsonfieldname - misspell - modernize - musttag @@ -27,6 +26,7 @@ linters: - revive - sliceofpointers - staticcheck + - structfield - tparallel - unconvert - unparam @@ -152,12 +152,16 @@ linters: type: module description: Reports usage of %d or %s in format strings. original-url: github.com/google/go-github/v79/tools/fmtpercentv - jsonfieldname: + sliceofpointers: + type: module + description: Reports usage of []*string and slices of structs without pointers. + original-url: github.com/google/go-github/v79/tools/sliceofpointers + structfield: type: module - description: Reports mismatches between Go field and JSON tag names. - original-url: github.com/google/go-github/v79/tools/jsonfieldname + description: Reports mismatches between Go field and JSON, URL tag names and types. + original-url: github.com/google/go-github/v79/tools/structfield settings: - allowed-exceptions: + allowed-tag-names: - ActionsCacheUsageList.RepoCacheUsage # TODO: RepoCacheUsages ? - AuditEntry.ExternalIdentityNameID - AuditEntry.Timestamp @@ -186,6 +190,8 @@ linters: - ListCheckSuiteResults.Total - ListCustomDeploymentRuleIntegrationsResponse.AvailableIntegrations - ListDeploymentProtectionRuleResponse.ProtectionRules + - ListIDPGroupsOptions.Query + - ListProjectsOptions.Query - OrganizationCustomRepoRoles.CustomRepoRoles # TODO: CustomRoles - OrganizationCustomRoles.CustomRepoRoles # TODO: Roles - PreReceiveHook.ConfigURL @@ -219,10 +225,201 @@ linters: - WeeklyStats.Commits - WeeklyStats.Deletions - WeeklyStats.Week - sliceofpointers: - type: module - description: Reports usage of []*string and slices of structs without pointers. - original-url: github.com/google/go-github/v79/tools/sliceofpointers + allowed-tag-types: + - ActivityListStarredOptions.Direction # TODO: Activities + - ActivityListStarredOptions.Sort # TODO: Activities + - AddProjectItemOptions.ID # TODO: Projects + - AddProjectItemOptions.Type # TODO: Projects + - AlertInstancesListOptions.Ref # TODO: CodeScanning + - AlertListOptions.Direction # TODO: CodeScanning + - AlertListOptions.Ref # TODO: CodeScanning + - AlertListOptions.Severity # TODO: CodeScanning + - AlertListOptions.Sort # TODO: CodeScanning + - AlertListOptions.State # TODO: CodeScanning + - AlertListOptions.ToolGUID # TODO: CodeScanning + - AlertListOptions.ToolName # TODO: CodeScanning + - APIMetaArtifactAttestations.TrustDomain # TODO: Meta + - CommitsListOptions.Author # TODO: Repositories + - CommitsListOptions.Path # TODO: Repositories + - CommitsListOptions.SHA # TODO: Repositories + - CommitsListOptions.Since # TODO: Repositories + - CommitsListOptions.Until # TODO: Repositories + - CreateTag.Message # TODO: Git + - CreateTag.Object # TODO: Git + - CreateTag.Tag # TODO: Git + - CreateTag.Type # TODO: Git + - CredentialAuthorizationsListOptions.Login # TODO: Organizations + - DependabotEncryptedSecret.SelectedRepositoryIDs # TODO: Dependabot + - DependabotEncryptedSecret.Visibility # TODO: Dependabot + - DeploymentRequest.RequiredContexts # TODO: Deployments + - DeploymentsListOptions.Environment # TODO: Repositories + - DeploymentsListOptions.Ref # TODO: Repositories + - DeploymentsListOptions.SHA # TODO: Repositories + - DeploymentsListOptions.Task # TODO: Repositories + - DiscussionCommentListOptions.Direction # TODO: Teams + - DiscussionListOptions.Direction # TODO: Teams + - DismissalRestrictionsRequest.Apps # TODO: Repositories + - DismissalRestrictionsRequest.Teams # TODO: Repositories + - DismissalRestrictionsRequest.Users # TODO: Repositories + - EncryptedSecret.SelectedRepositoryIDs # TODO: Actions + - EncryptedSecret.Visibility # TODO: Actions + - ErrorBlock.Reason # TODO: Common + - ErrorResponse.DocumentationURL # TODO: Common + - GetCodeownersErrorsOptions.Ref # TODO: Repositories + - GistListOptions.Since # TODO: Gists + - HostedRunnerRequest.EnableStaticIP # TODO: Actions + - HostedRunnerRequest.Image # TODO: Actions + - HostedRunnerRequest.ImageVersion # TODO: Actions + - HostedRunnerRequest.MaximumRunners # TODO: Actions + - HostedRunnerRequest.Name # TODO: Actions + - HostedRunnerRequest.RunnerGroupID # TODO: Actions + - HostedRunnerRequest.Size # TODO: Actions + - IssueEvent.Action # TODO: Issues + - IssueListByRepoOptions.Assignee # TODO: Issues + - IssueListByRepoOptions.Assignee # TODO: Issues + - IssueListByRepoOptions.Creator # TODO: Issues + - IssueListByRepoOptions.Creator # TODO: Issues + - IssueListByRepoOptions.Direction # TODO: Issues + - IssueListByRepoOptions.Direction # TODO: Issues + - IssueListByRepoOptions.Mentioned # TODO: Issues + - IssueListByRepoOptions.Mentioned # TODO: Issues + - IssueListByRepoOptions.Milestone # TODO: Issues + - IssueListByRepoOptions.Since # TODO: Issues + - IssueListByRepoOptions.Since # TODO: Issues + - IssueListByRepoOptions.Sort # TODO: Issues + - IssueListByRepoOptions.Sort # TODO: Issues + - IssueListByRepoOptions.State # TODO: Issues + - IssueListOptions.Direction # TODO: Issues + - IssueListOptions.Filter # TODO: Issues + - IssueListOptions.Since # TODO: Issues + - IssueListOptions.Sort # TODO: Issues + - IssueListOptions.State # TODO: Issues + - IssueRequest.Assignees # TODO: Issues + - IssueRequest.Labels # TODO: Issues + - License.Conditions # TODO: Licenses + - License.Limitations # TODO: Licenses + - License.Permissions # TODO: Licenses + - ListCodespacesOptions.RepositoryID # TODO: Codespaces + - ListCollaboratorsOptions.Affiliation # TODO: Repositories + - ListCollaboratorsOptions.Permission # TODO: Repositories + - ListContributorsOptions.Anon # TODO: Repositories + - ListCursorOptions.After # TODO: Common + - ListCursorOptions.Before # TODO: Common + - ListCursorOptions.Cursor # TODO: Common + - ListCursorOptions.First # TODO: Common + - ListCursorOptions.Last # TODO: Common + - ListCursorOptions.Page # TODO: Common + - ListCursorOptions.PerPage # TODO: Common + - ListCustomPropertyValuesOptions.RepositoryQuery # TODO: Organizations + - ListEnterpriseRunnerGroupOptions.VisibleToOrganization # TODO: Enterprise + - ListFineGrainedPATOptions.Direction # TODO: Organizations + - ListFineGrainedPATOptions.LastUsedAfter # TODO: Organizations + - ListFineGrainedPATOptions.LastUsedBefore # TODO: Organizations + - ListFineGrainedPATOptions.Permission # TODO: Organizations + - ListFineGrainedPATOptions.Repository # TODO: Organizations + - ListFineGrainedPATOptions.Sort # TODO: Organizations + - ListIDPGroupsOptions.Query # TODO: Teams + - ListMembersOptions.Filter # TODO: Organizations + - ListMembersOptions.Role # TODO: Organizations + - ListOptions.Page # TODO: Common + - ListOptions.PerPage # TODO: Common + - ListOrgMembershipsOptions.State # TODO: Organizations + - ListOrgRunnerGroupOptions.VisibleToRepository # TODO: Actions + - ListOutsideCollaboratorsOptions.Filter # TODO: Organizations + - ListProvisionedSCIMGroupsEnterpriseOptions.Count # TODO: Enterprise + - ListProvisionedSCIMGroupsEnterpriseOptions.ExcludedAttributes # TODO: Enterprise + - ListProvisionedSCIMGroupsEnterpriseOptions.Filter # TODO: Enterprise + - ListProvisionedSCIMGroupsEnterpriseOptions.StartIndex # TODO: Enterprise + - ListReactionOptions.Content # TODO: Reactions + - ListRepositoryActivityOptions.ActivityType # TODO: Repositories + - ListRepositoryActivityOptions.Actor # TODO: Repositories + - ListRepositoryActivityOptions.After # TODO: Repositories + - ListRepositoryActivityOptions.Before # TODO: Repositories + - ListRepositoryActivityOptions.Direction # TODO: Repositories + - ListRepositoryActivityOptions.PerPage # TODO: Repositories + - ListRepositoryActivityOptions.Ref # TODO: Repositories + - ListRepositoryActivityOptions.TimePeriod # TODO: Repositories + - ListRepositorySecurityAdvisoriesOptions.Direction # TODO: SecurityAdvisories + - ListRepositorySecurityAdvisoriesOptions.Direction # TODO: SecurityAdvisories + - ListRepositorySecurityAdvisoriesOptions.Sort # TODO: SecurityAdvisories + - ListRepositorySecurityAdvisoriesOptions.Sort # TODO: SecurityAdvisories + - ListRepositorySecurityAdvisoriesOptions.State # TODO: SecurityAdvisories + - ListRepositorySecurityAdvisoriesOptions.State # TODO: SecurityAdvisories + - ListWorkflowJobsOptions.Filter # TODO: Actions + - ListWorkflowRunsOptions.Actor # TODO: Actions + - ListWorkflowRunsOptions.Branch # TODO: Actions + - ListWorkflowRunsOptions.CheckSuiteID # TODO: Actions + - ListWorkflowRunsOptions.Created # TODO: Actions + - ListWorkflowRunsOptions.Event # TODO: Actions + - ListWorkflowRunsOptions.ExcludePullRequests # TODO: Actions + - ListWorkflowRunsOptions.HeadSHA # TODO: Actions + - ListWorkflowRunsOptions.Status # TODO: Actions + - LockIssueOptions.LockReason # TODO: Issues + - MarketplacePlan.Bullets # TODO: Marketplaces + - MilestoneListOptions.Direction # TODO: Issues + - MilestoneListOptions.Sort # TODO: Issues + - MilestoneListOptions.State # TODO: Issues + - NotificationListOptions.All # TODO: Activities + - NotificationListOptions.Before # TODO: Activities + - NotificationListOptions.Participating # TODO: Activities + - NotificationListOptions.Since # TODO: Activities + - OrganizationsListOptions.Since # TODO: Organizations + - ProjectV2ItemFieldValue.DataType # TODO: Projects + - ProjectV2ItemFieldValue.Name # TODO: Projects + - PullRequestListCommentsOptions.Direction # TODO: PullRequests + - PullRequestListCommentsOptions.Since # TODO: PullRequests + - PullRequestListCommentsOptions.Sort # TODO: PullRequests + - PullRequestListOptions.Base # TODO: PullRequests + - PullRequestListOptions.Direction # TODO: PullRequests + - PullRequestListOptions.Head # TODO: PullRequests + - PullRequestListOptions.Sort # TODO: PullRequests + - PullRequestListOptions.State # TODO: PullRequests + - Rate.Resource # TODO: Common + - RepositoryAddCollaboratorOptions.Permission # TODO: Repositories + - RepositoryContentGetOptions.Ref # TODO: Repositories + - RepositoryCreateForkOptions.DefaultBranchOnly # TODO: Repositories + - RepositoryCreateForkOptions.Name # TODO: Repositories + - RepositoryCreateForkOptions.Organization # TODO: Repositories + - RepositoryListAllOptions.Since # TODO: Repositories + - RepositoryListByAuthenticatedUserOptions.Affiliation # TODO: Repositories + - RepositoryListByAuthenticatedUserOptions.Direction # TODO: Repositories + - RepositoryListByAuthenticatedUserOptions.Sort # TODO: Repositories + - RepositoryListByAuthenticatedUserOptions.Type # TODO: Repositories + - RepositoryListByAuthenticatedUserOptions.Visibility # TODO: Repositories + - RepositoryListByOrgOptions.Direction # TODO: Repositories + - RepositoryListByOrgOptions.Sort # TODO: Repositories + - RepositoryListByOrgOptions.Type # TODO: Repositories + - RepositoryListByUserOptions.Direction # TODO: Repositories + - RepositoryListByUserOptions.Sort # TODO: Repositories + - RepositoryListByUserOptions.Type # TODO: Repositories + - RepositoryListForksOptions.Sort # TODO: Repositories + - RepositoryListOptions.Affiliation # TODO: Repositories + - RepositoryListOptions.Direction # TODO: Repositories + - RepositoryListOptions.Sort # TODO: Repositories + - RepositoryListOptions.Type # TODO: Repositories + - RepositoryListOptions.Visibility # TODO: Repositories + - RequiredStatusChecks.Checks # TODO: Repositories + - RequiredStatusChecks.Contexts # TODO: Repositories + - SearchOptions.Order # TODO: Search + - SearchOptions.Sort # TODO: Search + - Secret.SelectedRepositoriesURL # TODO: Actions + - Secret.Visibility # TODO: Actions + - SecretScanningAlertListOptions.Direction # TODO: SecretScanning + - SecretScanningAlertListOptions.IsMultiRepo # TODO: SecretScanning + - SecretScanningAlertListOptions.IsPubliclyLeaked # TODO: SecretScanning + - SecretScanningAlertListOptions.Resolution # TODO: SecretScanning + - SecretScanningAlertListOptions.SecretType # TODO: SecretScanning + - SecretScanningAlertListOptions.Sort # TODO: SecretScanning + - SecretScanningAlertListOptions.State # TODO: SecretScanning + - SecretScanningAlertListOptions.Validity # TODO: SecretScanning + - TeamAddTeamMembershipOptions.Role # TODO: Teams + - TeamAddTeamRepoOptions.Permission # TODO: Teams + - TeamListTeamMembersOptions.Role # TODO: Teams + - TrafficBreakdownOptions.Per # TODO: Repositories + - UpdateRuleParameters.UpdateAllowsFetchAndMerge # TODO: Rules + - UploadOptions.Label # TODO: Repositories + - UploadOptions.Name # TODO: Repositories + - UserListOptions.Since # TODO: Users exclusions: rules: - linters: @@ -258,6 +455,9 @@ linters: # Because fmt.Sprint(reset.Unix())) is more readable than strconv.FormatInt(reset.Unix(), 10). - linters: [perfsprint] text: fmt.Sprint.* can be replaced with faster strconv.FormatInt +issues: + max-issues-per-linter: 0 + max-same-issues: 0 formatters: enable: - gci diff --git a/tools/jsonfieldname/jsonfieldname.go b/tools/jsonfieldname/jsonfieldname.go deleted file mode 100644 index 1de10bc2f8c..00000000000 --- a/tools/jsonfieldname/jsonfieldname.go +++ /dev/null @@ -1,234 +0,0 @@ -// 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 jsonfieldname is a custom linter to be used by -// golangci-lint to find instances where the Go field name -// of a struct does not match the JSON tag name. -// It honors idiomatic Go initialisms and handles the -// special case of `Github` vs `GitHub` as agreed upon -// by the original author of the repo. -package jsonfieldname - -import ( - "go/ast" - "go/token" - "reflect" - "regexp" - "strings" - - "github.com/golangci/plugin-module-register/register" - "golang.org/x/tools/go/analysis" -) - -func init() { - register.Plugin("jsonfieldname", New) -} - -// JSONFieldNamePlugin is a custom linter plugin for golangci-lint. -type JSONFieldNamePlugin struct { - allowedExceptions map[string]bool -} - -// Settings is the configuration for the jsonfieldname linter. -type Settings struct { - AllowedExceptions []string `json:"allowed-exceptions" yaml:"allowed-exceptions"` -} - -// New returns an analysis.Analyzer to use with golangci-lint. -// It parses the "allowed-exceptions" section to determine which warnings to skip. -func New(cfg any) (register.LinterPlugin, error) { - allowedExceptions := map[string]bool{} - - if cfg != nil { - if settingsMap, ok := cfg.(map[string]any); ok { - if exceptionsRaw, ok := settingsMap["allowed-exceptions"]; ok { - if exceptionsList, ok := exceptionsRaw.([]any); ok { - for _, item := range exceptionsList { - if exception, ok := item.(string); ok { - allowedExceptions[exception] = true - } - } - } - } - } - } - - return &JSONFieldNamePlugin{allowedExceptions: allowedExceptions}, nil -} - -// BuildAnalyzers builds the analyzers for the JSONFieldNamePlugin. -func (f *JSONFieldNamePlugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { - return []*analysis.Analyzer{ - { - Name: "jsonfieldname", - Doc: "Reports mismatches between Go field and JSON tag names. Note that the JSON tag name is the source-of-truth and the Go field name needs to match it.", - Run: func(pass *analysis.Pass) (any, error) { - return run(pass, f.allowedExceptions) - }, - }, - }, nil -} - -// GetLoadMode returns the load mode for the JSONFieldNamePlugin. -func (f *JSONFieldNamePlugin) GetLoadMode() string { - return register.LoadModeSyntax -} - -func run(pass *analysis.Pass, allowedExceptions map[string]bool) (any, error) { - for _, file := range pass.Files { - ast.Inspect(file, func(n ast.Node) bool { - if n == nil { - return false - } - - switch t := n.(type) { - case *ast.TypeSpec: - structType, ok := t.Type.(*ast.StructType) - if !ok { - return true - } - structName := t.Name.Name - - // Only check exported structs. - if !ast.IsExported(structName) { - return true - } - - for _, field := range structType.Fields.List { - if field.Tag == nil || len(field.Names) == 0 { - continue - } - - goField := field.Names[0] - tagValue := strings.Trim(field.Tag.Value, "`") - structTag := reflect.StructTag(tagValue) - jsonTagName, ok := structTag.Lookup("json") - if !ok || jsonTagName == "-" { - continue - } - jsonTagName = strings.TrimSuffix(jsonTagName, ",omitempty") - - checkGoFieldName(structName, goField.Name, jsonTagName, goField.Pos(), pass, allowedExceptions) - } - } - - return true - }) - } - return nil, nil -} - -func checkGoFieldName(structName, goFieldName, jsonTagName string, tokenPos token.Pos, pass *analysis.Pass, allowedExceptions map[string]bool) { - fullName := structName + "." + goFieldName - if allowedExceptions[fullName] { - return - } - - want, alternate := jsonTagToPascal(jsonTagName) - if goFieldName != want && goFieldName != alternate { - const msg = "change Go field name %q to %q for JSON tag %q in struct %q" - pass.Reportf(tokenPos, msg, goFieldName, want, jsonTagName, structName) - } -} - -func splitJSONTag(jsonTagName string) []string { - jsonTagName = strings.TrimPrefix(jsonTagName, "$") - - if strings.Contains(jsonTagName, "_") { - return strings.Split(jsonTagName, "_") - } - - if strings.Contains(jsonTagName, "-") { - return strings.Split(jsonTagName, "-") - } - - if strings.ToLower(jsonTagName) == jsonTagName { // single word - return []string{jsonTagName} - } - - s := camelCaseRE.ReplaceAllString(jsonTagName, "$1 $2") - parts := strings.Fields(s) - for i, part := range parts { - parts[i] = strings.ToLower(part) - } - - return parts -} - -var camelCaseRE = regexp.MustCompile(`([a-z0-9])([A-Z])`) - -func jsonTagToPascal(jsonTagName string) (want, alternate string) { - parts := splitJSONTag(jsonTagName) - alt := make([]string, len(parts)) - for i, part := range parts { - alt[i] = part - if part == "" { - continue - } - upper := strings.ToUpper(part) - if initialisms[upper] { - parts[i] = upper - alt[i] = upper - } else if specialCase, ok := specialCases[upper]; ok { - parts[i] = specialCase - alt[i] = specialCase - } else if possibleAlternate, ok := possibleAlternates[upper]; ok { - parts[i] = possibleAlternate - alt[i] = strings.ToUpper(part[:1]) + part[1:] - } else { - parts[i] = strings.ToUpper(part[:1]) + part[1:] - alt[i] = parts[i] - } - } - return strings.Join(parts, ""), strings.Join(alt, "") -} - -// Common Go initialisms that should be all caps. -var initialisms = map[string]bool{ - "API": true, "ASCII": true, - "CAA": true, "CAS": true, "CNAME": true, "CPU": true, - "CSS": true, "CWE": true, "CVE": true, "CVSS": true, - "DN": true, "DNS": true, - "EOF": true, "EPSS": true, - "GB": true, "GHSA": true, "GPG": true, "GUID": true, - "HTML": true, "HTTP": true, "HTTPS": true, - "ID": true, "IDE": true, "IDP": true, "IP": true, "JIT": true, - "JSON": true, - "LDAP": true, "LFS": true, "LHS": true, - "MD5": true, "MS": true, "MX": true, - "NPM": true, "NTP": true, "NVD": true, - "OID": true, "OS": true, - "PEM": true, "PR": true, "QPS": true, - "RAM": true, "RHS": true, "RPC": true, - "SAML": true, "SBOM": true, "SCIM": true, - "SHA": true, "SHA1": true, "SHA256": true, - "SKU": true, "SLA": true, "SMTP": true, "SNMP": true, - "SPDX": true, "SPDXID": true, "SQL": true, "SSH": true, - "SSL": true, "SSO": true, "SVN": true, - "TCP": true, "TFVC": true, "TLS": true, "TTL": true, - "UDP": true, "UI": true, "UID": true, "UUID": true, - "URI": true, "URL": true, "UTF8": true, - "VCF": true, "VCS": true, "VM": true, - "XML": true, "XMPP": true, "XSRF": true, "XSS": true, -} - -var specialCases = map[string]string{ - "CPUS": "CPUs", - "CWES": "CWEs", - "GRAPHQL": "GraphQL", - "HREF": "HRef", - "IDS": "IDs", - "IPS": "IPs", - "OAUTH": "OAuth", - "OPENAPI": "OpenAPI", - "URLS": "URLs", -} - -var possibleAlternates = map[string]string{ - "ORGANIZATION": "Org", - "ORGANIZATIONS": "Orgs", - "REPOSITORY": "Repo", - "REPOSITORIES": "Repos", -} diff --git a/tools/jsonfieldname/testdata/src/has-warnings/main.go b/tools/jsonfieldname/testdata/src/has-warnings/main.go deleted file mode 100644 index c1a74c6059a..00000000000 --- a/tools/jsonfieldname/testdata/src/has-warnings/main.go +++ /dev/null @@ -1,14 +0,0 @@ -// 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 - -type Example struct { - GitHubThing string `json:"github_thing"` // want `change Go field name "GitHubThing" to "GithubThing" for JSON tag "github_thing" in struct "Example"` - Id string `json:"id,omitempty"` // want `change Go field name "Id" to "ID" for JSON tag "id" in struct "Example"` - strings string `json:"strings,omitempty"` // want `change Go field name "strings" to "Strings" for JSON tag "strings" in struct "Example"` - camelcaseexample *int `json:"camelCaseExample,omitempty"` // want `change Go field name "camelcaseexample" to "CamelCaseExample" for JSON tag "camelCaseExample" in struct "Example"` - DollarRef string `json:"$ref"` // want `change Go field name "DollarRef" to "Ref" for JSON tag "\$ref" in struct "Example"` -} diff --git a/tools/jsonfieldname/testdata/src/no-warnings/main.go b/tools/jsonfieldname/testdata/src/no-warnings/main.go deleted file mode 100644 index c522c7783ea..00000000000 --- a/tools/jsonfieldname/testdata/src/no-warnings/main.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 - -type Example struct { - GithubThing string `json:"github_thing"` // Should not be flagged - ID string `json:"id,omitempty"` // Should not be flagged - Strings string `json:"strings,omitempty"` // Should not be flagged - Ref string `json:"$ref,omitempty"` // Should not be flagged -} diff --git a/tools/jsonfieldname/go.mod b/tools/structfield/go.mod similarity index 87% rename from tools/jsonfieldname/go.mod rename to tools/structfield/go.mod index 3ae53f27ac4..22571c72626 100644 --- a/tools/jsonfieldname/go.mod +++ b/tools/structfield/go.mod @@ -1,4 +1,4 @@ -module tools/jsonfieldname +module tools/structfield go 1.24.0 diff --git a/tools/jsonfieldname/go.sum b/tools/structfield/go.sum similarity index 100% rename from tools/jsonfieldname/go.sum rename to tools/structfield/go.sum diff --git a/tools/structfield/structfield.go b/tools/structfield/structfield.go new file mode 100644 index 00000000000..19ac0db4de8 --- /dev/null +++ b/tools/structfield/structfield.go @@ -0,0 +1,362 @@ +// 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 structfield is a custom linter to be used by +// golangci-lint to find instances where the Go field name +// of a struct does not match the JSON or URL tag name. +// It honors idiomatic Go initialisms and handles the +// special case of `Github` vs `GitHub` as agreed upon +// by the original author of the repo. +// It also checks that fields with "omitempty" tags are reference types. +package structfield + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "regexp" + "strings" + + "github.com/golangci/plugin-module-register/register" + "golang.org/x/tools/go/analysis" +) + +func init() { + register.Plugin("structfield", New) +} + +// StructFieldPlugin is a custom linter plugin for golangci-lint. +type StructFieldPlugin struct { + allowedTagNames map[string]bool + allowedTagTypes map[string]bool +} + +// Settings is the configuration for the structfield linter. +type Settings struct { + AllowedTagNames []string `json:"allowed-tag-names" yaml:"allowed-tag-names"` + AllowedTagTypes []string `json:"allowed-tag-types" yaml:"allowed-tag-types"` +} + +// New returns an analysis.Analyzer to use with golangci-lint. +func New(cfg any) (register.LinterPlugin, error) { + allowedTagNames := map[string]bool{} + allowedTagTypes := map[string]bool{} + + if cfg != nil { + if settingsMap, ok := cfg.(map[string]any); ok { + if exceptionsRaw, ok := settingsMap["allowed-tag-names"]; ok { + if exceptionsList, ok := exceptionsRaw.([]any); ok { + for _, item := range exceptionsList { + if exception, ok := item.(string); ok { + allowedTagNames[exception] = true + } + } + } + } + + if exceptionsRaw, ok := settingsMap["allowed-tag-types"]; ok { + if exceptionsList, ok := exceptionsRaw.([]any); ok { + for _, item := range exceptionsList { + if exception, ok := item.(string); ok { + allowedTagTypes[exception] = true + } + } + } + } + } + } + + return &StructFieldPlugin{ + allowedTagNames: allowedTagNames, + allowedTagTypes: allowedTagTypes, + }, nil +} + +// BuildAnalyzers builds the analyzers for the StructFieldPlugin. +func (f *StructFieldPlugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { + return []*analysis.Analyzer{ + { + Name: "structfield", + Doc: `Reports mismatches between Go field and JSON or URL tag names and types. +Note that the JSON or URL tag name is the source-of-truth and the Go field name needs to match it. +If the tag contains "omitempty", then the Go field must be a reference type.`, + Run: func(pass *analysis.Pass) (any, error) { + return run(pass, f.allowedTagNames, f.allowedTagTypes) + }, + }, + }, nil +} + +// GetLoadMode returns the load mode for the StructFieldPlugin. +func (f *StructFieldPlugin) GetLoadMode() string { + return register.LoadModeSyntax +} + +func run(pass *analysis.Pass, allowedTagNames, allowedTagTypes map[string]bool) (any, error) { + for _, file := range pass.Files { + ast.Inspect(file, func(n ast.Node) bool { + if n == nil { + return false + } + + t, ok := n.(*ast.TypeSpec) + if !ok { + return true + } + structType, ok := t.Type.(*ast.StructType) + if !ok { + return true + } + + // Check only exported + if !ast.IsExported(t.Name.Name) { + return true + } + + for _, field := range structType.Fields.List { + if field.Tag == nil || len(field.Names) == 0 { + continue + } + + processStructField(t.Name.Name, field, pass, allowedTagNames, allowedTagTypes) + } + + return true + }) + } + return nil, nil +} + +func processStructField(structName string, field *ast.Field, pass *analysis.Pass, allowedTagNames, allowedTagTypes map[string]bool) { + goField := field.Names[0] + tagValue := strings.Trim(field.Tag.Value, "`") + structTag := reflect.StructTag(tagValue) + + processTag(structName, goField, field, structTag, "json", pass, allowedTagNames, allowedTagTypes) + processTag(structName, goField, field, structTag, "url", pass, allowedTagNames, allowedTagTypes) +} + +func processTag(structName string, goField *ast.Ident, field *ast.Field, structTag reflect.StructTag, tagType string, pass *analysis.Pass, allowedTagNames, allowedTagTypes map[string]bool) { + tagName, ok := structTag.Lookup(tagType) + if !ok || tagName == "-" { + return + } + + if strings.Contains(tagName, ",omitempty") { + checkGoFieldType(structName, goField.Name, field, field.Type.Pos(), pass, allowedTagTypes) + tagName = strings.ReplaceAll(tagName, ",omitempty", "") + } + + if tagType == "url" { + tagName = strings.ReplaceAll(tagName, ",comma", "") + } + + checkGoFieldName(structName, goField.Name, tagName, goField.Pos(), pass, allowedTagNames) +} + +func checkGoFieldName(structName, goFieldName, tagName string, tokenPos token.Pos, pass *analysis.Pass, allowedNames map[string]bool) { + fullName := structName + "." + goFieldName + if allowedNames[fullName] { + return + } + + want, alternate := tagNameToPascal(tagName) + if goFieldName != want && goFieldName != alternate { + const msg = "change Go field name %q to %q for tag %q in struct %q" + pass.Reportf(tokenPos, msg, goFieldName, want, tagName, structName) + } +} + +func checkGoFieldType(structName, goFieldName string, field *ast.Field, tokenPos token.Pos, pass *analysis.Pass, allowedTypes map[string]bool) { + if allowedTypes[structName+"."+goFieldName] { + return + } + + skipOmitempty := checkAndReportInvalidTypes(structName, goFieldName, field.Type, tokenPos, pass) + + if !skipOmitempty { + const msg = `change the %q field type to %q in the struct %q because its tag uses "omitempty"` + pass.Reportf(tokenPos, msg, goFieldName, "*"+exprToString(field.Type), structName) + } +} + +func checkAndReportInvalidTypes(structName, goFieldName string, fieldType ast.Expr, tokenPos token.Pos, pass *analysis.Pass) bool { + switch ft := fieldType.(type) { + case *ast.StarExpr: + // Check for *[]T where T is builtin - should be []T + if arrType, ok := ft.X.(*ast.ArrayType); ok { + if ident, ok := arrType.Elt.(*ast.Ident); ok && isBuiltinType(ident.Name) { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]"+ident.Name, structName) + } else if starExpr, ok := arrType.Elt.(*ast.StarExpr); ok { + // Check for *[]*T - should be []*T + if ident, ok := starExpr.X.(*ast.Ident); ok { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]*"+ident.Name, structName) + } + } else { + checkStructArrayType(structName, goFieldName, arrType, tokenPos, pass) + } + } + // Check for *map - should be map + if _, ok := ft.X.(*ast.MapType); ok { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, exprToString(ft.X), structName) + } + return true + case *ast.MapType: + return true + case *ast.ArrayType: + checkStructArrayType(structName, goFieldName, ft, tokenPos, pass) + return true + case *ast.SelectorExpr: + // Check for json.RawMessage + if ident, ok := ft.X.(*ast.Ident); ok && ident.Name == "json" && ft.Sel.Name == "RawMessage" { + return true + } + case *ast.Ident: + // Check for `any` type + if ft.Name == "any" { + return true + } + } + return false +} + +func checkStructArrayType(structName, goFieldName string, arrType *ast.ArrayType, tokenPos token.Pos, pass *analysis.Pass) { + if starExpr, ok := arrType.Elt.(*ast.StarExpr); ok { + if ident, ok := starExpr.X.(*ast.Ident); ok && isBuiltinType(ident.Name) { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]"+ident.Name, structName) + } + return + } + + if ident, ok := arrType.Elt.(*ast.Ident); ok && ident.Obj != nil { + if _, ok := ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.StructType); ok { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]*"+ident.Name, structName) + } + } +} + +func isBuiltinType(typeName string) bool { + return types.Universe.Lookup(typeName) != nil +} + +func exprToString(e ast.Expr) string { + switch t := e.(type) { + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return exprToString(t.X) + "." + t.Sel.Name + case *ast.MapType: + return "map[" + exprToString(t.Key) + "]" + exprToString(t.Value) + default: + return fmt.Sprintf("%T", e) + } +} + +func splitTag(jsonTagName string) []string { + jsonTagName = strings.TrimPrefix(jsonTagName, "$") + + if strings.Contains(jsonTagName, "_") { + return strings.Split(jsonTagName, "_") + } + + if strings.Contains(jsonTagName, "-") { + return strings.Split(jsonTagName, "-") + } + + if strings.ToLower(jsonTagName) == jsonTagName { // single word + return []string{jsonTagName} + } + + s := camelCaseRE.ReplaceAllString(jsonTagName, "$1 $2") + parts := strings.Fields(s) + for i, part := range parts { + parts[i] = strings.ToLower(part) + } + + return parts +} + +var camelCaseRE = regexp.MustCompile(`([a-z0-9])([A-Z])`) + +func tagNameToPascal(tagName string) (want, alternate string) { + parts := splitTag(tagName) + alt := make([]string, len(parts)) + for i, part := range parts { + alt[i] = part + if part == "" { + continue + } + upper := strings.ToUpper(part) + if initialisms[upper] { + parts[i] = upper + alt[i] = upper + } else if specialCase, ok := specialCases[upper]; ok { + parts[i] = specialCase + alt[i] = specialCase + } else if possibleAlternate, ok := possibleAlternates[upper]; ok { + parts[i] = possibleAlternate + alt[i] = strings.ToUpper(part[:1]) + part[1:] + } else { + parts[i] = strings.ToUpper(part[:1]) + part[1:] + alt[i] = parts[i] + } + } + return strings.Join(parts, ""), strings.Join(alt, "") +} + +// Common Go initialisms that should be all caps. +var initialisms = map[string]bool{ + "API": true, "ASCII": true, + "CAA": true, "CAS": true, "CNAME": true, "CPU": true, + "CSS": true, "CWE": true, "CVE": true, "CVSS": true, + "DN": true, "DNS": true, + "EOF": true, "EPSS": true, + "GB": true, "GHSA": true, "GPG": true, "GUID": true, + "HTML": true, "HTTP": true, "HTTPS": true, + "ID": true, "IDE": true, "IDP": true, "IP": true, "JIT": true, + "JSON": true, + "LDAP": true, "LFS": true, "LHS": true, + "MD5": true, "MS": true, "MX": true, + "NPM": true, "NTP": true, "NVD": true, + "OID": true, "OS": true, + "PEM": true, "PR": true, "QPS": true, + "RAM": true, "RHS": true, "RPC": true, + "SAML": true, "SBOM": true, "SCIM": true, + "SHA": true, "SHA1": true, "SHA256": true, + "SKU": true, "SLA": true, "SMTP": true, "SNMP": true, + "SPDX": true, "SPDXID": true, "SQL": true, "SSH": true, + "SSL": true, "SSO": true, "SVN": true, + "TCP": true, "TFVC": true, "TLS": true, "TTL": true, + "UDP": true, "UI": true, "UID": true, "UUID": true, + "URI": true, "URL": true, "UTF8": true, + "VCF": true, "VCS": true, "VM": true, + "XML": true, "XMPP": true, "XSRF": true, "XSS": true, +} + +var specialCases = map[string]string{ + "CPUS": "CPUs", + "CWES": "CWEs", + "GRAPHQL": "GraphQL", + "HREF": "HRef", + "IDS": "IDs", + "IPS": "IPs", + "OAUTH": "OAuth", + "OPENAPI": "OpenAPI", + "URLS": "URLs", +} + +var possibleAlternates = map[string]string{ + "ORGANIZATION": "Org", + "ORGANIZATIONS": "Orgs", + "REPOSITORY": "Repo", + "REPOSITORIES": "Repos", +} diff --git a/tools/jsonfieldname/jsonfieldname_test.go b/tools/structfield/structfield_test.go similarity index 64% rename from tools/jsonfieldname/jsonfieldname_test.go rename to tools/structfield/structfield_test.go index fa5044c7aba..a192782313a 100644 --- a/tools/jsonfieldname/jsonfieldname_test.go +++ b/tools/structfield/structfield_test.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package jsonfieldname +package structfield import ( "testing" @@ -14,7 +14,16 @@ import ( func TestRun(t *testing.T) { t.Parallel() testdata := analysistest.TestData() - plugin, _ := New(nil) + plugin, _ := New(map[string]any{ + "allowed-tag-names": []any{ + "JSONFieldName.Query", + "URLFieldName.Query", + }, + "allowed-tag-types": []any{ + "JSONFieldType.Exception", + "URLFieldType.Exception", + }, + }) analyzers, _ := plugin.BuildAnalyzers() analysistest.Run(t, testdata, analyzers[0], "has-warnings", "no-warnings") } diff --git a/tools/structfield/testdata/src/has-warnings/main.go b/tools/structfield/testdata/src/has-warnings/main.go new file mode 100644 index 00000000000..26d2704fc78 --- /dev/null +++ b/tools/structfield/testdata/src/has-warnings/main.go @@ -0,0 +1,37 @@ +// 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 + +type JSONFieldName struct { + GitHubThing string `json:"github_thing"` // want `change Go field name "GitHubThing" to "GithubThing" for tag "github_thing" in struct "JSONFieldName"` + Id *string `json:"id,omitempty"` // want `change Go field name "Id" to "ID" for tag "id" in struct "JSONFieldName"` + strings *string `json:"strings,omitempty"` // want `change Go field name "strings" to "Strings" for tag "strings" in struct "JSONFieldName"` + camelcaseexample *int `json:"camelCaseExample,omitempty"` // want `change Go field name "camelcaseexample" to "CamelCaseExample" for tag "camelCaseExample" in struct "JSONFieldName"` + DollarRef string `json:"$ref"` // want `change Go field name "DollarRef" to "Ref" for tag "\$ref" in struct "JSONFieldName"` +} + +type JSONFieldType struct { + String string `json:"string,omitempty"` // want `change the "String" field type to "\*string" in the struct "JSONFieldType" because its tag uses "omitempty"` + SliceOfStringPointers []*string `json:"slice_of_string_pointers,omitempty"` // want `change the "SliceOfStringPointers" field type to "\[\]string" in the struct "JSONFieldType"` + PointerToSliceOfStrings *[]string `json:"pointer_to_slice_of_strings,omitempty"` // want `change the "PointerToSliceOfStrings" field type to "\[\]string" in the struct "JSONFieldType"` + SliceOfStructs []Struct `json:"slice_of_structs,omitempty"` // want `change the "SliceOfStructs" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + PointerToSliceOfStructs *[]Struct `json:"pointer_to_slice_of_structs,omitempty"` // want `change the "PointerToSliceOfStructs" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + PointerToSliceOfPointerStructs *[]*Struct `json:"pointer_to_slice_of_pointer_structs,omitempty"` // want `change the "PointerToSliceOfPointerStructs" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + PointerToMap *map[string]string `json:"pointer_to_map,omitempty"` // want `change the "PointerToMap" field type to "map\[string\]string" in the struct "JSONFieldType"` + SliceOfInts []*int `json:"slice_of_ints,omitempty"` // want `change the "SliceOfInts" field type to "\[\]int" in the struct "JSONFieldType"` +} + +type Struct struct{} + +type URLFieldName struct { + GitHubThing string `url:"github_thing"` // want `change Go field name "GitHubThing" to "GithubThing" for tag "github_thing" in struct "URLFieldName"` +} + +type URLFieldType struct { + Page string `url:"page,omitempty"` // want `change the "Page" field type to "\*string" in the struct "URLFieldType" because its tag uses "omitempty"` + PerPage int `url:"per_page,omitempty"` // want `change the "PerPage" field type to "\*int" in the struct "URLFieldType" because its tag uses "omitempty"` + Participating bool `url:"participating,omitempty"` // want `change the "Participating" field type to "\*bool" in the struct "URLFieldType" because its tag uses "omitempty"` +} diff --git a/tools/structfield/testdata/src/no-warnings/main.go b/tools/structfield/testdata/src/no-warnings/main.go new file mode 100644 index 00000000000..fc0236f1cfe --- /dev/null +++ b/tools/structfield/testdata/src/no-warnings/main.go @@ -0,0 +1,46 @@ +// 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 ( + "encoding/json" + "time" +) + +type JSONFieldName struct { + GithubThing string `json:"github_thing"` + ID *string `json:"id,omitempty"` + Strings *string `json:"strings,omitempty"` + Ref *string `json:"$ref,omitempty"` + Query string `json:"q"` +} + +type JSONFieldType struct { + WithoutTag string + + ID *string `json:"id,omitempty"` + HookAttributes map[string]string `json:"hook_attributes,omitempty"` + Inputs json.RawMessage `json:"inputs,omitempty"` + Exception string `json:"exception,omitempty"` + Value any `json:"value,omitempty"` + SliceOfPointerStructs []*Struct `json:"slice_of_pointer_structs,omitempty"` +} + +type URLFieldName struct { + ID *string `url:"id,omitempty"` + Query string `url:"q"` +} + +type URLFieldType struct { + Page *string `url:"page,omitempty"` + PerPage *int `url:"per_page,omitempty"` + Labels []string `url:"labels,omitempty,comma"` + Since *time.Time `url:"since,omitempty"` + Fields []int64 `url:"fields,omitempty,comma"` + Exception string `url:"exception,omitempty"` +} + +type Struct struct{} From 7db3132a8f6e4473f02a84e95dd69b05dd99272a Mon Sep 17 00:00:00 2001 From: cointem Date: Tue, 2 Dec 2025 02:14:49 +0800 Subject: [PATCH 12/49] feat: Add Credentials Revoke API (#3847) --- github/credentials.go | 37 +++++++++++++++++++++++++++++ github/credentials_test.go | 48 ++++++++++++++++++++++++++++++++++++++ github/github.go | 2 ++ 3 files changed, 87 insertions(+) create mode 100644 github/credentials.go create mode 100644 github/credentials_test.go diff --git a/github/credentials.go b/github/credentials.go new file mode 100644 index 00000000000..ff6e6f6913a --- /dev/null +++ b/github/credentials.go @@ -0,0 +1,37 @@ +// 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" +) + +// CredentialsService handles credentials related methods of the GitHub API. +type CredentialsService service + +// revokeCredentialsRequest represents the request body for revoking credentials. +type revokeCredentialsRequest struct { + // The list of credential strings (tokens) to revoke. + Credentials []string `json:"credentials"` +} + +// Revoke revokes a list of credentials. +// +// GitHub API docs: https://docs.github.com/rest/credentials/revoke#revoke-a-list-of-credentials +// +//meta:operation POST /credentials/revoke +func (s *CredentialsService) Revoke(ctx context.Context, credentials []string) (*Response, error) { + u := "credentials/revoke" + + reqBody := &revokeCredentialsRequest{Credentials: credentials} + + req, err := s.client.NewRequest("POST", u, reqBody) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/github/credentials_test.go b/github/credentials_test.go new file mode 100644 index 00000000000..997c33fc9ec --- /dev/null +++ b/github/credentials_test.go @@ -0,0 +1,48 @@ +// 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 ( + "encoding/json" + "errors" + "net/http" + "testing" +) + +func TestCredentialsService_Revoke(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + creds := []string{ + "ghp_1234567890abcdef1234567890abcdef12345678", + "ghp_abcdef1234567890abcdef1234567890abcdef12", + } + expectedBodyBytes, _ := json.Marshal(map[string][]string{"credentials": creds}) + expectedBody := string(expectedBodyBytes) + "\n" + + mux.HandleFunc("/credentials/revoke", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, expectedBody) + w.WriteHeader(http.StatusAccepted) + }) + + ctx := t.Context() + resp, err := client.Credentials.Revoke(ctx, creds) + if !errors.As(err, new(*AcceptedError)) { + t.Errorf("Credentials.Revoke returned error: %v (want AcceptedError)", err) + } + if resp == nil { + t.Fatal("Credentials.Revoke returned nil response") + } + if resp.StatusCode != http.StatusAccepted { + t.Errorf("Credentials.Revoke returned status %v, want %v", resp.StatusCode, http.StatusAccepted) + } + + const methodName = "Revoke" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Credentials.Revoke(ctx, []string{"a"}) + }) +} diff --git a/github/github.go b/github/github.go index eba55176108..b9efa3b4e89 100644 --- a/github/github.go +++ b/github/github.go @@ -203,6 +203,7 @@ type Client struct { CodesOfConduct *CodesOfConductService Codespaces *CodespacesService Copilot *CopilotService + Credentials *CredentialsService Dependabot *DependabotService DependencyGraph *DependencyGraphService Emojis *EmojisService @@ -445,6 +446,7 @@ func (c *Client) initialize() { c.Codespaces = (*CodespacesService)(&c.common) c.CodesOfConduct = (*CodesOfConductService)(&c.common) c.Copilot = (*CopilotService)(&c.common) + c.Credentials = (*CredentialsService)(&c.common) c.Dependabot = (*DependabotService)(&c.common) c.DependencyGraph = (*DependencyGraphService)(&c.common) c.Emojis = (*EmojisService)(&c.common) From d14f4f3d8ded409d16a7b1ef9cf0b5ccf3066303 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Mon, 1 Dec 2025 21:55:37 +0200 Subject: [PATCH 13/49] docs: Improve displaying GitHub API links on pkg.go.dev (#3845) --- github/activity_events.go | 2 ++ github/activity_star.go | 1 + github/activity_watching.go | 1 + github/apps.go | 1 + github/apps_marketplace.go | 4 ++++ github/gists.go | 1 + github/issues.go | 1 + github/issues_comments.go | 1 + github/orgs.go | 1 + github/orgs_members.go | 3 +++ github/pulls_comments.go | 1 + github/repos.go | 2 ++ github/repos_contents.go | 1 + github/users.go | 1 + github/users_followers.go | 3 +++ github/users_gpg_keys.go | 1 + github/users_keys.go | 1 + github/users_packages.go | 8 ++++++++ github/users_ssh_signing_keys.go | 1 + tools/metadata/metadata.go | 6 +++++- .../testdata/golden/TestUpdateGo/valid/github/a.go | 10 ++++++++++ tools/metadata/testdata/update-go/valid/github/a.go | 6 ++++++ .../testdata/update-go/valid/openapi_operations.yaml | 4 ++++ 23 files changed, 60 insertions(+), 1 deletion(-) diff --git a/github/activity_events.go b/github/activity_events.go index b12baa99e67..597f7992092 100644 --- a/github/activity_events.go +++ b/github/activity_events.go @@ -143,6 +143,7 @@ func (s *ActivityService) ListEventsForOrganization(ctx context.Context, org str // true, only public events will be returned. // // GitHub API docs: https://docs.github.com/rest/activity/events#list-events-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/activity/events#list-public-events-for-a-user // //meta:operation GET /users/{username}/events @@ -177,6 +178,7 @@ func (s *ActivityService) ListEventsPerformedByUser(ctx context.Context, user st // true, only public events will be returned. // // GitHub API docs: https://docs.github.com/rest/activity/events#list-events-received-by-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/activity/events#list-public-events-received-by-a-user // //meta:operation GET /users/{username}/received_events diff --git a/github/activity_star.go b/github/activity_star.go index bc68dfef2dd..d9383e434ed 100644 --- a/github/activity_star.go +++ b/github/activity_star.go @@ -69,6 +69,7 @@ type ActivityListStarredOptions struct { // will list the starred repositories for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/activity/starring#list-repositories-starred-by-a-user +// // GitHub API docs: https://docs.github.com/rest/activity/starring#list-repositories-starred-by-the-authenticated-user // //meta:operation GET /user/starred diff --git a/github/activity_watching.go b/github/activity_watching.go index 61429177862..8106d799cf1 100644 --- a/github/activity_watching.go +++ b/github/activity_watching.go @@ -55,6 +55,7 @@ func (s *ActivityService) ListWatchers(ctx context.Context, owner, repo string, // the empty string will fetch watched repos for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/activity/watching#list-repositories-watched-by-a-user +// // GitHub API docs: https://docs.github.com/rest/activity/watching#list-repositories-watched-by-the-authenticated-user // //meta:operation GET /user/subscriptions diff --git a/github/apps.go b/github/apps.go index e1bede64717..fe36b9584dc 100644 --- a/github/apps.go +++ b/github/apps.go @@ -215,6 +215,7 @@ func (i Installation) String() string { // (e.g., https://github.com/settings/apps/:app_slug). // // GitHub API docs: https://docs.github.com/rest/apps/apps#get-an-app +// // GitHub API docs: https://docs.github.com/rest/apps/apps#get-the-authenticated-app // //meta:operation GET /app diff --git a/github/apps_marketplace.go b/github/apps_marketplace.go index 976775a79a0..33382378e2e 100644 --- a/github/apps_marketplace.go +++ b/github/apps_marketplace.go @@ -90,6 +90,7 @@ type MarketplacePurchaseAccount struct { // ListPlans lists all plans for your Marketplace listing. // // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-plans +// // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-plans-stubbed // //meta:operation GET /marketplace_listing/plans @@ -118,6 +119,7 @@ func (s *MarketplaceService) ListPlans(ctx context.Context, opts *ListOptions) ( // ListPlanAccountsForPlan lists all GitHub accounts (user or organization) on a specific plan. // // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-accounts-for-a-plan +// // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-accounts-for-a-plan-stubbed // //meta:operation GET /marketplace_listing/plans/{plan_id}/accounts @@ -146,6 +148,7 @@ func (s *MarketplaceService) ListPlanAccountsForPlan(ctx context.Context, planID // GetPlanAccountForAccount get GitHub account (user or organization) associated with an account. // // GitHub API docs: https://docs.github.com/rest/apps/marketplace#get-a-subscription-plan-for-an-account +// // GitHub API docs: https://docs.github.com/rest/apps/marketplace#get-a-subscription-plan-for-an-account-stubbed // //meta:operation GET /marketplace_listing/accounts/{account_id} @@ -170,6 +173,7 @@ func (s *MarketplaceService) GetPlanAccountForAccount(ctx context.Context, accou // ListMarketplacePurchasesForUser lists all GitHub marketplace purchases made by a user. // // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-subscriptions-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/apps/marketplace#list-subscriptions-for-the-authenticated-user-stubbed // //meta:operation GET /user/marketplace_purchases diff --git a/github/gists.go b/github/gists.go index ee4314b986e..8b5dd47d8ec 100644 --- a/github/gists.go +++ b/github/gists.go @@ -97,6 +97,7 @@ type GistListOptions struct { // user. // // GitHub API docs: https://docs.github.com/rest/gists/gists#list-gists-for-a-user +// // GitHub API docs: https://docs.github.com/rest/gists/gists#list-gists-for-the-authenticated-user // //meta:operation GET /gists diff --git a/github/issues.go b/github/issues.go index d19987f26bf..861c3fa5f90 100644 --- a/github/issues.go +++ b/github/issues.go @@ -160,6 +160,7 @@ type IssueType struct { // repositories. // // GitHub API docs: https://docs.github.com/rest/issues/issues#list-issues-assigned-to-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/issues/issues#list-user-account-issues-assigned-to-the-authenticated-user // //meta:operation GET /issues diff --git a/github/issues_comments.go b/github/issues_comments.go index 35069400de8..ef5314b188d 100644 --- a/github/issues_comments.go +++ b/github/issues_comments.go @@ -55,6 +55,7 @@ type IssueListCommentsOptions struct { // number of 0 will return all comments on all issues for the repository. // // GitHub API docs: https://docs.github.com/rest/issues/comments#list-issue-comments +// // GitHub API docs: https://docs.github.com/rest/issues/comments#list-issue-comments-for-a-repository // //meta:operation GET /repos/{owner}/{repo}/issues/comments diff --git a/github/orgs.go b/github/orgs.go index c1f7bf0d028..185d0393039 100644 --- a/github/orgs.go +++ b/github/orgs.go @@ -194,6 +194,7 @@ func (s *OrganizationsService) ListAll(ctx context.Context, opts *OrganizationsL // organizations for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/orgs/orgs#list-organizations-for-a-user +// // GitHub API docs: https://docs.github.com/rest/orgs/orgs#list-organizations-for-the-authenticated-user // //meta:operation GET /user/orgs diff --git a/github/orgs_members.go b/github/orgs_members.go index 002811c6eac..837a8f44677 100644 --- a/github/orgs_members.go +++ b/github/orgs_members.go @@ -72,6 +72,7 @@ type ListMembersOptions struct { // public members; otherwise, it will only return public members. // // GitHub API docs: https://docs.github.com/rest/orgs/members#list-organization-members +// // GitHub API docs: https://docs.github.com/rest/orgs/members#list-public-organization-members // //meta:operation GET /orgs/{org}/members @@ -237,6 +238,7 @@ func (s *OrganizationsService) ListOrgMemberships(ctx context.Context, opts *Lis // authenticated user. // // GitHub API docs: https://docs.github.com/rest/orgs/members#get-an-organization-membership-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/orgs/members#get-organization-membership-for-a-user // //meta:operation GET /orgs/{org}/memberships/{username} @@ -268,6 +270,7 @@ func (s *OrganizationsService) GetOrgMembership(ctx context.Context, user, org s // authenticated user. // // GitHub API docs: https://docs.github.com/rest/orgs/members#set-organization-membership-for-a-user +// // GitHub API docs: https://docs.github.com/rest/orgs/members#update-an-organization-membership-for-the-authenticated-user // //meta:operation PUT /orgs/{org}/memberships/{username} diff --git a/github/pulls_comments.go b/github/pulls_comments.go index 03b3c1d2057..4d601730b48 100644 --- a/github/pulls_comments.go +++ b/github/pulls_comments.go @@ -73,6 +73,7 @@ type PullRequestListCommentsOptions struct { // the repository. // // GitHub API docs: https://docs.github.com/rest/pulls/comments#list-review-comments-in-a-repository +// // GitHub API docs: https://docs.github.com/rest/pulls/comments#list-review-comments-on-a-pull-request // //meta:operation GET /repos/{owner}/{repo}/pulls/comments diff --git a/github/repos.go b/github/repos.go index 0119a01b5c4..24444273edc 100644 --- a/github/repos.go +++ b/github/repos.go @@ -263,6 +263,7 @@ type SecretScanningValidityChecks struct { // Deprecated: Use RepositoriesService.ListByUser or RepositoriesService.ListByAuthenticatedUser instead. // // GitHub API docs: https://docs.github.com/rest/repos/repos#list-repositories-for-a-user +// // GitHub API docs: https://docs.github.com/rest/repos/repos#list-repositories-for-the-authenticated-user // //meta:operation GET /user/repos @@ -530,6 +531,7 @@ type createRepoRequest struct { // exponential back-off to verify repository's creation. // // GitHub API docs: https://docs.github.com/rest/repos/repos#create-a-repository-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/repos/repos#create-an-organization-repository // //meta:operation POST /orgs/{org}/repos diff --git a/github/repos_contents.go b/github/repos_contents.go index 2378cd2330e..6090c8b9f92 100644 --- a/github/repos_contents.go +++ b/github/repos_contents.go @@ -353,6 +353,7 @@ const ( // or github.Zipball constant. // // GitHub API docs: https://docs.github.com/rest/repos/contents#download-a-repository-archive-tar +// // GitHub API docs: https://docs.github.com/rest/repos/contents#download-a-repository-archive-zip // //meta:operation GET /repos/{owner}/{repo}/tarball/{ref} diff --git a/github/users.go b/github/users.go index 87dcc9a44d3..b3bd3b09b07 100644 --- a/github/users.go +++ b/github/users.go @@ -88,6 +88,7 @@ func (u User) String() string { // user. // // GitHub API docs: https://docs.github.com/rest/users/users#get-a-user +// // GitHub API docs: https://docs.github.com/rest/users/users#get-the-authenticated-user // //meta:operation GET /user diff --git a/github/users_followers.go b/github/users_followers.go index ec6f531eaa4..6833d8df646 100644 --- a/github/users_followers.go +++ b/github/users_followers.go @@ -14,6 +14,7 @@ import ( // fetch followers for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/users/followers#list-followers-of-a-user +// // GitHub API docs: https://docs.github.com/rest/users/followers#list-followers-of-the-authenticated-user // //meta:operation GET /user/followers @@ -48,6 +49,7 @@ func (s *UsersService) ListFollowers(ctx context.Context, user string, opts *Lis // string will list people the authenticated user is following. // // GitHub API docs: https://docs.github.com/rest/users/followers#list-the-people-a-user-follows +// // GitHub API docs: https://docs.github.com/rest/users/followers#list-the-people-the-authenticated-user-follows // //meta:operation GET /user/following @@ -82,6 +84,7 @@ func (s *UsersService) ListFollowing(ctx context.Context, user string, opts *Lis // string for "user" will check if the authenticated user is following "target". // // GitHub API docs: https://docs.github.com/rest/users/followers#check-if-a-person-is-followed-by-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/users/followers#check-if-a-user-follows-another-user // //meta:operation GET /user/following/{username} diff --git a/github/users_gpg_keys.go b/github/users_gpg_keys.go index 2f296a1ef62..d33564cab7c 100644 --- a/github/users_gpg_keys.go +++ b/github/users_gpg_keys.go @@ -45,6 +45,7 @@ type GPGEmail struct { // via Basic Auth or via OAuth with at least read:gpg_key scope. // // GitHub API docs: https://docs.github.com/rest/users/gpg-keys#list-gpg-keys-for-a-user +// // GitHub API docs: https://docs.github.com/rest/users/gpg-keys#list-gpg-keys-for-the-authenticated-user // //meta:operation GET /user/gpg_keys diff --git a/github/users_keys.go b/github/users_keys.go index 4d42986ed2b..350a684b797 100644 --- a/github/users_keys.go +++ b/github/users_keys.go @@ -31,6 +31,7 @@ func (k Key) String() string { // string will fetch keys for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/users/keys#list-public-keys-for-a-user +// // GitHub API docs: https://docs.github.com/rest/users/keys#list-public-ssh-keys-for-the-authenticated-user // //meta:operation GET /user/keys diff --git a/github/users_packages.go b/github/users_packages.go index b813dd9d14f..fa6cd9157c1 100644 --- a/github/users_packages.go +++ b/github/users_packages.go @@ -15,6 +15,7 @@ import ( // list packages for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#list-packages-for-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#list-packages-for-the-authenticated-users-namespace // //meta:operation GET /user/packages @@ -49,6 +50,7 @@ func (s *UsersService) ListPackages(ctx context.Context, user string, opts *Pack // get the package for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#get-a-package-for-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#get-a-package-for-the-authenticated-user // //meta:operation GET /user/packages/{package_type}/{package_name} @@ -79,6 +81,7 @@ func (s *UsersService) GetPackage(ctx context.Context, user, packageType, packag // delete the package for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#delete-a-package-for-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#delete-a-package-for-the-authenticated-user // //meta:operation DELETE /user/packages/{package_type}/{package_name} @@ -103,6 +106,7 @@ func (s *UsersService) DeletePackage(ctx context.Context, user, packageType, pac // restore the package for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#restore-a-package-for-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#restore-a-package-for-the-authenticated-user // //meta:operation POST /user/packages/{package_type}/{package_name}/restore @@ -127,6 +131,7 @@ func (s *UsersService) RestorePackage(ctx context.Context, user, packageType, pa // get versions for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#list-package-versions-for-a-package-owned-by-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#list-package-versions-for-a-package-owned-by-the-authenticated-user // //meta:operation GET /user/packages/{package_type}/{package_name}/versions @@ -161,6 +166,7 @@ func (s *UsersService) PackageGetAllVersions(ctx context.Context, user, packageT // get the version for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#get-a-package-version-for-a-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#get-a-package-version-for-the-authenticated-user // //meta:operation GET /user/packages/{package_type}/{package_name}/versions/{package_version_id} @@ -191,6 +197,7 @@ func (s *UsersService) PackageGetVersion(ctx context.Context, user, packageType, // delete the version for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#delete-a-package-version-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#delete-package-version-for-a-user // //meta:operation DELETE /user/packages/{package_type}/{package_name}/versions/{package_version_id} @@ -215,6 +222,7 @@ func (s *UsersService) PackageDeleteVersion(ctx context.Context, user, packageTy // restore the version for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/packages/packages#restore-a-package-version-for-the-authenticated-user +// // GitHub API docs: https://docs.github.com/rest/packages/packages#restore-package-version-for-a-user // //meta:operation POST /user/packages/{package_type}/{package_name}/versions/{package_version_id}/restore diff --git a/github/users_ssh_signing_keys.go b/github/users_ssh_signing_keys.go index fcc930be628..968401f028c 100644 --- a/github/users_ssh_signing_keys.go +++ b/github/users_ssh_signing_keys.go @@ -26,6 +26,7 @@ func (k SSHSigningKey) String() string { // username string will fetch SSH signing keys for the authenticated user. // // GitHub API docs: https://docs.github.com/rest/users/ssh-signing-keys#list-ssh-signing-keys-for-a-user +// // GitHub API docs: https://docs.github.com/rest/users/ssh-signing-keys#list-ssh-signing-keys-for-the-authenticated-user // //meta:operation GET /user/ssh_signing_keys diff --git a/tools/metadata/metadata.go b/tools/metadata/metadata.go index 6391a6ffb83..1050e598e61 100644 --- a/tools/metadata/metadata.go +++ b/tools/metadata/metadata.go @@ -333,13 +333,17 @@ func updateDocsVisitor(opsFile *operationsFile) nodeVisitor { } sort.Strings(docLinks) - for _, dl := range docLinks { + for i, dl := range docLinks { group.List = append( group.List, &ast.Comment{ Text: "// GitHub API docs: " + cleanURLPath(dl), }, ) + if i < len(docLinks)-1 { + // add empty line between doc links + group.List = append(group.List, &ast.Comment{Text: "//"}) + } } _, methodName, _ := strings.Cut(serviceMethod, ".") for _, opName := range undocumentedOps { diff --git a/tools/metadata/testdata/golden/TestUpdateGo/valid/github/a.go b/tools/metadata/testdata/golden/TestUpdateGo/valid/github/a.go index 571c1eb2da9..310fc4b9532 100644 --- a/tools/metadata/testdata/golden/TestUpdateGo/valid/github/a.go +++ b/tools/metadata/testdata/golden/TestUpdateGo/valid/github/a.go @@ -28,6 +28,16 @@ func (s *AService) OutdatedLinks() {} //meta:operation GET /a/{a_id} func (s *AService) Uncommented() {} +// Get gets a user. +// +// GitHub API docs: https://docs.github.com/rest/users/users#get-a-user +// +// GitHub API docs: https://docs.github.com/rest/users/users#get-the-authenticated-user +// +//meta:operation GET /user +//meta:operation GET /users/{username} +func (s *AService) Get(user string) {} + func (s *AService) unexported() {} func NotAMethod() {} diff --git a/tools/metadata/testdata/update-go/valid/github/a.go b/tools/metadata/testdata/update-go/valid/github/a.go index 245e2ba1df1..63af01975b0 100644 --- a/tools/metadata/testdata/update-go/valid/github/a.go +++ b/tools/metadata/testdata/update-go/valid/github/a.go @@ -24,6 +24,12 @@ func (s *AService) OutdatedLinks() {} //meta:operation GET /a/{a_id} func (s *AService) Uncommented() {} +// Get gets a user. +// +//meta:operation GET /user +//meta:operation GET /users/{username} +func (s *AService) Get(user string) {} + func (s *AService) unexported() {} func NotAMethod() {} diff --git a/tools/metadata/testdata/update-go/valid/openapi_operations.yaml b/tools/metadata/testdata/update-go/valid/openapi_operations.yaml index ed401c0d283..dfa8af18a47 100644 --- a/tools/metadata/testdata/update-go/valid/openapi_operations.yaml +++ b/tools/metadata/testdata/update-go/valid/openapi_operations.yaml @@ -6,6 +6,10 @@ openapi_operations: - name: GET /a/{a_id} documentation_url: https://docs.github.com/rest/a/a#get-a - name: GET /undocumented/{undocumented_id} + - name: GET /user + documentation_url: https://docs.github.com/rest/users/users#get-a-user + - name: GET /users/{username} + documentation_url: https://docs.github.com/rest/users/users#get-the-authenticated-user operation_overrides: - name: GET /a/{a_id} documentation_url: https://docs.github.com/rest/a/a#overridden-get-a From b480d827a46622f93384e1633c510d49473c8a87 Mon Sep 17 00:00:00 2001 From: Nithish S <128178765+nithish-95@users.noreply.github.com> Date: Tue, 2 Dec 2025 05:05:21 -0800 Subject: [PATCH 14/49] feat: Add GitHub Enterprise App installation repository management APIs (#3831) --- github/enterprise_apps.go | 116 ++++++++++++++++++++++++ github/enterprise_apps_test.go | 155 ++++++++++++++++++++++++++++++++ github/github-accessors.go | 8 ++ github/github-accessors_test.go | 11 +++ 4 files changed, 290 insertions(+) create mode 100644 github/enterprise_apps.go create mode 100644 github/enterprise_apps_test.go diff --git a/github/enterprise_apps.go b/github/enterprise_apps.go new file mode 100644 index 00000000000..3fc0df7436d --- /dev/null +++ b/github/enterprise_apps.go @@ -0,0 +1,116 @@ +// 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" +) + +// AppInstallationRepositoriesOptions specifies the parameters for +// EnterpriseService.AddRepositoriesToAppInstallation and +// EnterpriseService.RemoveRepositoriesFromAppInstallation. +type AppInstallationRepositoriesOptions struct { + SelectedRepositoryIDs []int64 `json:"selected_repository_ids"` +} + +// UpdateAppInstallationRepositoriesOptions specifies the parameters for +// EnterpriseService.UpdateAppInstallationRepositories. +type UpdateAppInstallationRepositoriesOptions struct { + RepositorySelection *string `json:"repository_selection,omitempty"` // Can be "all" or "selected" + SelectedRepositoryIDs []int64 `json:"selected_repository_ids,omitempty"` +} + +// ListRepositoriesForOrgAppInstallation lists the repositories that an enterprise app installation +// has access to on an organization. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#get-the-repositories-accessible-to-a-given-github-app-installation +// +//meta:operation GET /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories +func (s *EnterpriseService) ListRepositoriesForOrgAppInstallation(ctx context.Context, enterprise, org string, installationID int64, opts *ListOptions) ([]*AccessibleRepository, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v/repositories", enterprise, org, installationID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var r []*AccessibleRepository + resp, err := s.client.Do(ctx, req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, nil +} + +// UpdateAppInstallationRepositories changes a GitHub App installation's repository access +// between all repositories and a selected set. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#toggle-installation-repository-access-between-selected-and-all-repositories +// +//meta:operation PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories +func (s *EnterpriseService) UpdateAppInstallationRepositories(ctx context.Context, enterprise, org string, installationID int64, opts UpdateAppInstallationRepositoriesOptions) (*Installation, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v/repositories", enterprise, org, installationID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + + var r *Installation + resp, err := s.client.Do(ctx, req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, nil +} + +// AddRepositoriesToAppInstallation grants repository access for a GitHub App installation. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#grant-repository-access-to-an-organization-installation +// +//meta:operation PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/add +func (s *EnterpriseService) AddRepositoriesToAppInstallation(ctx context.Context, enterprise, org string, installationID int64, opts AppInstallationRepositoriesOptions) ([]*AccessibleRepository, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v/repositories/add", enterprise, org, installationID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + + var r []*AccessibleRepository + resp, err := s.client.Do(ctx, req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, nil +} + +// RemoveRepositoriesFromAppInstallation revokes repository access from a GitHub App installation. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/organization-installations#remove-repository-access-from-an-organization-installation +// +//meta:operation PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/remove +func (s *EnterpriseService) RemoveRepositoriesFromAppInstallation(ctx context.Context, enterprise, org string, installationID int64, opts AppInstallationRepositoriesOptions) ([]*AccessibleRepository, *Response, error) { + u := fmt.Sprintf("enterprises/%v/apps/organizations/%v/installations/%v/repositories/remove", enterprise, org, installationID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + + var r []*AccessibleRepository + resp, err := s.client.Do(ctx, req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, nil +} diff --git a/github/enterprise_apps_test.go b/github/enterprise_apps_test.go new file mode 100644 index 00000000000..5b53b28d8b6 --- /dev/null +++ b/github/enterprise_apps_test.go @@ -0,0 +1,155 @@ +// 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 ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEnterpriseService_ListRepositoriesForOrgAppInstallation(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/apps/organizations/o/installations/1/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "1"}) + fmt.Fprint(w, `[{"id":1}]`) + }) + + ctx := t.Context() + repos, _, err := client.Enterprise.ListRepositoriesForOrgAppInstallation(ctx, "e", "o", 1, &ListOptions{Page: 1}) + if err != nil { + t.Errorf("Enterprise.ListRepositoriesForOrgAppInstallation returned error: %v", err) + } + + want := []*AccessibleRepository{{ID: 1}} + if diff := cmp.Diff(repos, want); diff != "" { + t.Errorf("Enterprise.ListRepositoriesForOrgAppInstallation returned diff (-want +got):\n%v", diff) + } + + const methodName = "ListRepositoriesForOrgAppInstallation" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ListRepositoriesForOrgAppInstallation(ctx, "\n", "\n", -1, &ListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.Enterprise.ListRepositoriesForOrgAppInstallation(ctx, "e", "o", 1, &ListOptions{}) + return resp, err + }) +} + +func TestEnterpriseService_UpdateAppInstallationRepositories(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := UpdateAppInstallationRepositoriesOptions{ + RepositorySelection: String("selected"), + SelectedRepositoryIDs: []int64{1, 2}, + } + + mux.HandleFunc("/enterprises/e/apps/organizations/o/installations/1/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"repository_selection":"selected","selected_repository_ids":[1,2]}`+"\n") + fmt.Fprint(w, `{"id":1, "repository_selection":"selected"}`) + }) + + ctx := t.Context() + inst, _, err := client.Enterprise.UpdateAppInstallationRepositories(ctx, "e", "o", 1, input) + if err != nil { + t.Errorf("Enterprise.UpdateAppInstallationRepositories returned error: %v", err) + } + + want := &Installation{ID: Ptr(int64(1)), RepositorySelection: Ptr("selected")} + if diff := cmp.Diff(inst, want); diff != "" { + t.Errorf("Enterprise.UpdateAppInstallationRepositories returned diff (-want +got):\n%v", diff) + } + + const methodName = "UpdateAppInstallationRepositories" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.UpdateAppInstallationRepositories(ctx, "\n", "\n", -1, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.Enterprise.UpdateAppInstallationRepositories(ctx, "e", "o", 1, input) + return resp, err + }) +} + +func TestEnterpriseService_AddRepositoriesToAppInstallation(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AppInstallationRepositoriesOptions{SelectedRepositoryIDs: []int64{1, 2}} + + mux.HandleFunc("/enterprises/e/apps/organizations/o/installations/1/repositories/add", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"selected_repository_ids":[1,2]}`+"\n") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + ctx := t.Context() + repos, _, err := client.Enterprise.AddRepositoriesToAppInstallation(ctx, "e", "o", 1, input) + if err != nil { + t.Errorf("Enterprise.AddRepositoriesToAppInstallation returned error: %v", err) + } + + want := []*AccessibleRepository{{ID: 1}, {ID: 2}} + if diff := cmp.Diff(repos, want); diff != "" { + t.Errorf("Enterprise.AddRepositoriesToAppInstallation returned diff (-want +got):\n%v", diff) + } + + const methodName = "AddRepositoriesToAppInstallation" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.AddRepositoriesToAppInstallation(ctx, "\n", "\n", -1, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.Enterprise.AddRepositoriesToAppInstallation(ctx, "e", "o", 1, input) + return resp, err + }) +} + +func TestEnterpriseService_RemoveRepositoriesFromAppInstallation(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AppInstallationRepositoriesOptions{SelectedRepositoryIDs: []int64{1, 2}} + + mux.HandleFunc("/enterprises/e/apps/organizations/o/installations/1/repositories/remove", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"selected_repository_ids":[1,2]}`+"\n") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + ctx := t.Context() + repos, _, err := client.Enterprise.RemoveRepositoriesFromAppInstallation(ctx, "e", "o", 1, input) + if err != nil { + t.Errorf("Enterprise.RemoveRepositoriesFromAppInstallation returned error: %v", err) + } + + want := []*AccessibleRepository{{ID: 1}, {ID: 2}} + if diff := cmp.Diff(repos, want); diff != "" { + t.Errorf("Enterprise.RemoveRepositoriesFromAppInstallation returned diff (-want +got):\n%v", diff) + } + + const methodName = "RemoveRepositoriesFromAppInstallation" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.RemoveRepositoriesFromAppInstallation(ctx, "\n", "\n", -1, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.Enterprise.RemoveRepositoriesFromAppInstallation(ctx, "e", "o", 1, input) + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index b95be28e733..bef89aad127 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -29462,6 +29462,14 @@ func (t *TreeEntry) GetURL() string { return *t.URL } +// GetRepositorySelection returns the RepositorySelection field if it's non-nil, zero value otherwise. +func (u *UpdateAppInstallationRepositoriesOptions) GetRepositorySelection() string { + if u == nil || u.RepositorySelection == nil { + return "" + } + return *u.RepositorySelection +} + // GetPath returns the Path field if it's non-nil, zero value otherwise. func (u *UpdateAttributeForSCIMUserOperations) GetPath() string { if u == nil || u.Path == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 9abcb49dcde..f2dab20e030 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -37968,6 +37968,17 @@ func TestTreeEntry_GetURL(tt *testing.T) { t.GetURL() } +func TestUpdateAppInstallationRepositoriesOptions_GetRepositorySelection(tt *testing.T) { + tt.Parallel() + var zeroValue string + u := &UpdateAppInstallationRepositoriesOptions{RepositorySelection: &zeroValue} + u.GetRepositorySelection() + u = &UpdateAppInstallationRepositoriesOptions{} + u.GetRepositorySelection() + u = nil + u.GetRepositorySelection() +} + func TestUpdateAttributeForSCIMUserOperations_GetPath(tt *testing.T) { tt.Parallel() var zeroValue string From e279f702f81b2c1ad93ff4c2b531f2eb2143889a Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:00:16 +0100 Subject: [PATCH 15/49] feat: Implement Enterprise SCIM - Update Group & User attributes (#3848) --- github/enterprise_scim.go | 76 ++++- github/enterprise_scim_test.go | 512 +++++++++++++++++++++++--------- github/github-accessors.go | 16 + github/github-accessors_test.go | 22 ++ 4 files changed, 475 insertions(+), 151 deletions(-) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index 806954360a0..3243841a88c 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -22,14 +22,18 @@ const SCIMSchemasURINamespacesUser = "urn:ietf:params:scim:schemas:core:2.0:User // This constant represents the standard SCIM namespace for list responses used in paginated queries, as defined by RFC 7644. const SCIMSchemasURINamespacesListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse" -// SCIMEnterpriseGroupAttributes represents supported SCIM Enterprise group attributes. +// SCIMSchemasURINamespacesPatchOp is the SCIM schema URI namespace for patch operations. +// This constant represents the standard SCIM namespace for patch operations as defined by RFC 7644. +const SCIMSchemasURINamespacesPatchOp = "urn:ietf:params:scim:api:messages:2.0:PatchOp" + +// SCIMEnterpriseGroupAttributes represents supported SCIM Enterprise group attributes, and represents the result of calling UpdateSCIMGroupAttribute. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-group-attributes type SCIMEnterpriseGroupAttributes struct { DisplayName *string `json:"displayName,omitempty"` // Human-readable name for a group. Members []*SCIMEnterpriseDisplayReference `json:"members,omitempty"` // List of members who are assigned to the group in SCIM provider ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per user. - // Bellow: Only populated as a result of calling SetSCIMInformationForProvisionedGroup: + // Bellow: Only populated as a result of calling UpdateSCIMGroupAttribute: Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas. ID *string `json:"id,omitempty"` // The internally generated id for the group object. Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group. @@ -76,7 +80,8 @@ type ListProvisionedSCIMGroupsEnterpriseOptions struct { Count *int `url:"count,omitempty"` } -// SCIMEnterpriseUserAttributes represents supported SCIM enterprise user attributes. +// SCIMEnterpriseUserAttributes represents supported SCIM enterprise user attributes, and represents the result of calling UpdateSCIMUserAttribute. +// // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-user-attributes type SCIMEnterpriseUserAttributes struct { DisplayName string `json:"displayName"` // Human-readable name for a user @@ -87,7 +92,7 @@ type SCIMEnterpriseUserAttributes struct { ExternalID string `json:"externalId"` // This identifier is generated by a SCIM provider. Must be unique per user. Active bool `json:"active"` // Indicates whether the identity is active (true) or should be suspended (false). Schemas []string `json:"schemas"` // The URIs that are used to indicate the namespaces of the SCIM schemas. - // Bellow: Only populated as a result of calling SetSCIMInformationForProvisionedUser: + // Bellow: Only populated as a result of calling UpdateSCIMUserAttribute: ID *string `json:"id,omitempty"` // Identifier generated by the GitHub's SCIM endpoint. Groups []*SCIMEnterpriseDisplayReference `json:"groups,omitempty"` // List of groups who are assigned to the user in SCIM provider Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the user. @@ -116,7 +121,7 @@ type SCIMEnterpriseUserRole struct { Primary *bool `json:"primary,omitempty"` // Is the role a primary role for the user? } -// SCIMEnterpriseUsers represents the result of calling ProvisionSCIMEnterpriseUser. +// SCIMEnterpriseUsers represents the result of calling ListProvisionedSCIMUsers. type SCIMEnterpriseUsers struct { Schemas []string `json:"schemas,omitempty"` TotalResults *int `json:"totalResults,omitempty"` @@ -125,7 +130,7 @@ type SCIMEnterpriseUsers struct { Resources []*SCIMEnterpriseUserAttributes `json:"Resources,omitempty"` } -// ListProvisionedSCIMUsersEnterpriseOptions represents query parameters for ListSCIMProvisionedUsers. +// ListProvisionedSCIMUsersEnterpriseOptions represents query parameters for ListProvisionedSCIMUsers. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise type ListProvisionedSCIMUsersEnterpriseOptions struct { @@ -140,6 +145,21 @@ type ListProvisionedSCIMUsersEnterpriseOptions struct { Count *int `url:"count,omitempty"` } +// SCIMEnterpriseAttribute represents attribute operations for UpdateSCIMGroupAttribute or UpdateSCIMUserAttribute. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-group +type SCIMEnterpriseAttribute struct { + Schemas []string `json:"schemas"` // The URIs that are used to indicate the namespaces for a SCIM patch operation. + Operations []*SCIMEnterpriseAttributeOperation `json:"Operations"` // Set of operations to be performed. +} + +// SCIMEnterpriseAttributeOperation represents an operation for UpdateSCIMGroupAttribute or UpdateSCIMUserAttribute. +type SCIMEnterpriseAttributeOperation struct { + Op string `json:"op"` // Can be one of: `add`, `replace`, `remove`. + Path *string `json:"path,omitempty"` // Path to the attribute being modified (Filters are not supported). + Value *string `json:"value,omitempty"` // New value for the attribute being modified. +} + // ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise @@ -193,3 +213,47 @@ func (s *EnterpriseService) ListProvisionedSCIMUsers(ctx context.Context, enterp return users, resp, nil } + +// UpdateSCIMGroupAttribute updates a provisioned group’s individual attributes. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-group +// +//meta:operation PATCH /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} +func (s *EnterpriseService) UpdateSCIMGroupAttribute(ctx context.Context, enterprise, scimGroupID string, attribute SCIMEnterpriseAttribute) (*SCIMEnterpriseGroupAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Groups/%v", enterprise, scimGroupID) + req, err := s.client.NewRequest("PATCH", u, attribute) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + group := new(SCIMEnterpriseGroupAttributes) + resp, err := s.client.Do(ctx, req, group) + if err != nil { + return nil, resp, err + } + + return group, resp, nil +} + +// UpdateSCIMUserAttribute updates a provisioned user's individual attributes. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-user +// +//meta:operation PATCH /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} +func (s *EnterpriseService) UpdateSCIMUserAttribute(ctx context.Context, enterprise, scimUserID string, attribute SCIMEnterpriseAttribute) (*SCIMEnterpriseUserAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users/%v", enterprise, scimUserID) + req, err := s.client.NewRequest("PATCH", u, attribute) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + user := new(SCIMEnterpriseUserAttributes) + resp, err := s.client.Do(ctx, req, user) + if err != nil { + return nil, resp, err + } + + return user, resp, nil +} diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index adf9099ccea..4f65c27ff71 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -6,6 +6,7 @@ package github import ( + "fmt" "net/http" "testing" @@ -42,27 +43,27 @@ func TestSCIMEnterpriseGroups_Marshal(t *testing.T) { want := `{ "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], - "totalResults": 1, - "itemsPerPage": 1, - "startIndex": 1, - "Resources": [{ - "schemas": ["` + SCIMSchemasURINamespacesGroups + `"], - "id": "idgn1", - "externalId": "eidgn1", - "displayName": "gn1", - "meta": { - "resourceType": "Group", - "created": ` + referenceTimeStr + `, - "lastModified": ` + referenceTimeStr + `, - "location": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1" - }, - "members": [{ - "value": "idm1", - "$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/idm1", - "display": "m1" - }] - }] - }` + "totalResults": 1, + "itemsPerPage": 1, + "startIndex": 1, + "Resources": [{ + "schemas": ["` + SCIMSchemasURINamespacesGroups + `"], + "id": "idgn1", + "externalId": "eidgn1", + "displayName": "gn1", + "meta": { + "resourceType": "Group", + "created": ` + referenceTimeStr + `, + "lastModified": ` + referenceTimeStr + `, + "location": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1" + }, + "members": [{ + "value": "idm1", + "$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/idm1", + "display": "m1" + }] + }] + }` testJSONMarshal(t, u, want) } @@ -115,47 +116,47 @@ func TestSCIMEnterpriseUsers_Marshal(t *testing.T) { } want := `{ - "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], - "TotalResults": 1, - "itemsPerPage": 1, - "StartIndex": 1, - "Resources": [{ - "active": true, - "emails": [{ - "primary": true, - "type": "work", - "value": "un1@email.com" - }], - "roles": [{ - "display": "rd1", - "primary": true, - "type": "rt1", - "value": "rv1" - }], - "schemas": ["` + SCIMSchemasURINamespacesUser + `"], - "userName": "un1", - "groups": [{ - "value": "idgn1", - "$ref": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1", - "display": "gn1" - }], - "id": "idun1", - "externalId": "eidun1", - "name": { - "givenName": "gnn1", - "familyName": "fnn1", - "formatted": "f1", - "middleName": "mn1" - }, - "displayName": "dun1", - "meta": { - "resourceType": "User", - "created": ` + referenceTimeStr + `, - "lastModified": ` + referenceTimeStr + `, - "location": "https://api.github.com/scim/v2/enterprises/ee/User/idun1" - } - }] - }` + "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], + "TotalResults": 1, + "itemsPerPage": 1, + "StartIndex": 1, + "Resources": [{ + "active": true, + "emails": [{ + "primary": true, + "type": "work", + "value": "un1@email.com" + }], + "roles": [{ + "display": "rd1", + "primary": true, + "type": "rt1", + "value": "rv1" + }], + "schemas": ["` + SCIMSchemasURINamespacesUser + `"], + "userName": "un1", + "groups": [{ + "value": "idgn1", + "$ref": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1", + "display": "gn1" + }], + "id": "idun1", + "externalId": "eidun1", + "name": { + "givenName": "gnn1", + "familyName": "fnn1", + "formatted": "f1", + "middleName": "mn1" + }, + "displayName": "dun1", + "meta": { + "resourceType": "User", + "created": ` + referenceTimeStr + `, + "lastModified": ` + referenceTimeStr + `, + "location": "https://api.github.com/scim/v2/enterprises/ee/User/idun1" + } + }] + }` testJSONMarshal(t, u, want) } @@ -172,10 +173,10 @@ func TestListProvisionedSCIMGroupsEnterpriseOptions_Marshal(t *testing.T) { } want := `{ - "filter": "f", - "excludedAttributes": "ea", - "startIndex": 5, - "count": 9 + "filter": "f", + "excludedAttributes": "ea", + "startIndex": 5, + "count": 9 }` testJSONMarshal(t, u, want) @@ -192,9 +193,9 @@ func TestListProvisionedSCIMUsersEnterpriseOptions_Marshal(t *testing.T) { } want := `{ - "filter": "f", - "startIndex": 3, - "count": 7 + "filter": "f", + "startIndex": 3, + "count": 7 }` testJSONMarshal(t, u, want) @@ -223,15 +224,15 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) { } want := `{ - "schemas": ["s1"], - "externalId": "eid", + "schemas": ["s1"], + "externalId": "eid", "displayName": "dn", - "members" : [{ - "value": "v", - "$ref": "r", - "display": "d" - }], - "id": "id", + "members" : [{ + "value": "v", + "$ref": "r", + "display": "d" + }], + "id": "id", "meta": { "resourceType": "rt", "created": ` + referenceTimeStr + `, @@ -243,6 +244,39 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } +func TestSCIMEnterpriseAttribute_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &SCIMEnterpriseAttribute{}, "{}") + + u := &SCIMEnterpriseAttribute{ + Schemas: []string{"s"}, + Operations: []*SCIMEnterpriseAttributeOperation{ + { + Op: "o1", + Path: Ptr("p1"), + Value: Ptr("v1"), + }, + { + Op: "o2", + }, + }, + } + + want := `{ + "schemas": ["s"], + "Operations": [{ + "op": "o1", + "path": "p1", + "value": "v1" + }, + { + "op": "o2" + }] + }` + + testJSONMarshal(t, u, want) +} + func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -257,29 +291,29 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { "filter": `externalId eq "914a"`, }) w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], - "totalResults": 1, - "itemsPerPage": 1, - "startIndex": 1, - "Resources": [{ - "schemas": ["` + SCIMSchemasURINamespacesGroups + `"], - "id": "914a", - "externalId": "de88", - "displayName": "gn1", - "meta": { - "resourceType": "Group", - "created": ` + referenceTimeStr + `, - "lastModified": ` + referenceTimeStr + `, - "location": "https://api.github.com/scim/v2/enterprises/ee/Groups/914a" - }, - "members": [{ - "value": "e7f9", - "$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9", - "display": "d1" - }] - }] - }`)) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesListResponse+`"], + "totalResults": 1, + "itemsPerPage": 1, + "startIndex": 1, + "Resources": [{ + "schemas": ["`+SCIMSchemasURINamespacesGroups+`"], + "id": "914a", + "externalId": "de88", + "displayName": "gn1", + "meta": { + "resourceType": "Group", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.com/scim/v2/enterprises/ee/Groups/914a" + }, + "members": [{ + "value": "e7f9", + "$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9", + "display": "d1" + }] + }] + }`) }) ctx := t.Context() @@ -289,12 +323,12 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { Count: Ptr(3), Filter: Ptr(`externalId eq "914a"`), } - groups, _, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts) + got, _, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts) if err != nil { t.Fatalf("Enterprise.ListProvisionedSCIMGroups returned unexpected error: %v", err) } - want := SCIMEnterpriseGroups{ + want := &SCIMEnterpriseGroups{ Schemas: []string{SCIMSchemasURINamespacesListResponse}, TotalResults: Ptr(1), ItemsPerPage: Ptr(1), @@ -318,7 +352,7 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { }}, } - if diff := cmp.Diff(want, *groups); diff != "" { + if diff := cmp.Diff(want, got); diff != "" { t.Errorf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff) } @@ -329,8 +363,11 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - _, r, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "o", opts) - return r, err + got, resp, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err }) } @@ -338,7 +375,7 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - mux.HandleFunc("/scim/v2/enterprises/octo-org/Users", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/scim/v2/enterprises/ee/Users", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testHeader(t, r, "Accept", mediaTypeSCIM) testFormValues(t, r, values{ @@ -347,39 +384,39 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { "filter": `userName eq "octocat@github.com"`, }) w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{ - "schemas": ["` + SCIMSchemasURINamespacesListResponse + `"], - "totalResults": 1, - "itemsPerPage": 1, - "startIndex": 1, - "Resources": [ - { - "schemas": ["` + SCIMSchemasURINamespacesUser + `"], - "id": "5fc0", - "externalId": "00u1", - "userName": "octocat@github.com", - "displayName": "Mona Octocat", - "name": { - "givenName": "Mona", - "familyName": "Octocat", - "formatted": "Mona Octocat" - }, - "emails": [ - { - "value": "octocat@github.com", - "primary": true - } - ], - "active": true, - "meta": { - "resourceType": "User", - "created": ` + referenceTimeStr + `, - "lastModified": ` + referenceTimeStr + `, - "location": "https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0" - } - } - ] - }`)) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesListResponse+`"], + "totalResults": 1, + "itemsPerPage": 1, + "startIndex": 1, + "Resources": [ + { + "schemas": ["`+SCIMSchemasURINamespacesUser+`"], + "id": "5fc0", + "externalId": "00u1", + "userName": "octocat@github.com", + "displayName": "Mona Octocat", + "name": { + "givenName": "Mona", + "familyName": "Octocat", + "formatted": "Mona Octocat" + }, + "emails": [ + { + "value": "octocat@github.com", + "primary": true + } + ], + "active": true, + "meta": { + "resourceType": "User", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.com/scim/v2/enterprises/ee/Users/5fc0" + } + } + ] + }`) }) ctx := t.Context() @@ -388,12 +425,12 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { Count: Ptr(3), Filter: Ptr(`userName eq "octocat@github.com"`), } - users, _, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "octo-org", opts) + got, _, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "ee", opts) if err != nil { t.Fatalf("Enterprise.ListProvisionedSCIMUsers returned unexpected error: %v", err) } - want := SCIMEnterpriseUsers{ + want := &SCIMEnterpriseUsers{ Schemas: []string{SCIMSchemasURINamespacesListResponse}, TotalResults: Ptr(1), ItemsPerPage: Ptr(1), @@ -418,12 +455,12 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { ResourceType: "User", Created: &Timestamp{referenceTime}, LastModified: &Timestamp{referenceTime}, - Location: Ptr("https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0"), + Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/5fc0"), }, }}, } - if diff := cmp.Diff(want, *users); diff != "" { + if diff := cmp.Diff(want, got); diff != "" { t.Errorf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff) } @@ -434,7 +471,192 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - _, r, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "o", opts) - return r, err + got, resp, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "ee", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Groups/abcd", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"schemas":["`+SCIMSchemasURINamespacesPatchOp+`"],"Operations":[{"op":"replace","path":"displayName","value":"Employees"}]}`+"\n") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesGroups+`"], + "id": "abcd", + "externalId": "8aa1", + "displayName": "Employees", + "members": [{ + "value": "879d", + "$ref": "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d", + "display": "User 1" + }], + "meta": { + "resourceType": "Group", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd" + } + }`) + }) + want := &SCIMEnterpriseGroupAttributes{ + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ID: Ptr("abcd"), + ExternalID: Ptr("8aa1"), + DisplayName: Ptr("Employees"), + Members: []*SCIMEnterpriseDisplayReference{{ + Value: "879d", + Ref: "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d", + Display: Ptr("User 1"), + }}, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "Group", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseAttribute{ + Schemas: []string{SCIMSchemasURINamespacesPatchOp}, + Operations: []*SCIMEnterpriseAttributeOperation{{ + Op: "replace", + Path: Ptr("displayName"), + Value: Ptr("Employees"), + }}, + } + got, _, err := client.Enterprise.UpdateSCIMGroupAttribute(ctx, "ee", "abcd", input) + if err != nil { + t.Fatalf("Enterprise.UpdateSCIMGroupAttribute returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Enterprise.UpdateSCIMGroupAttribute diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "UpdateSCIMGroupAttribute" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.UpdateSCIMGroupAttribute(ctx, "\n", "\n", SCIMEnterpriseAttribute{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.UpdateSCIMGroupAttribute(ctx, "ee", "abcd", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Users/7fce", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"schemas":["`+SCIMSchemasURINamespacesPatchOp+`"],"Operations":[{"op":"replace","path":"emails[type eq 'work'].value","value":"updatedEmail@email.com"},{"op":"replace","path":"name.familyName","value":"updatedFamilyName"}]}`+"\n") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesUser+`"], + "id": "7fce", + "externalId": "e123", + "active": true, + "userName": "e123", + "name": { + "formatted": "John Doe X", + "familyName": "updatedFamilyName", + "givenName": "John", + "middleName": "X" + }, + "displayName": "John Doe", + "emails": [{ + "value": "john@email.com", + "type": "work", + "primary": true + }], + "roles": [{ + "value": "User", + "primary": false + }], + "meta": { + "resourceType": "User", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce" + } + }`) + }) + want := &SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ID: Ptr("7fce"), + ExternalID: "e123", + Active: true, + UserName: "e123", + DisplayName: "John Doe", + Name: &SCIMEnterpriseUserName{ + Formatted: Ptr("John Doe X"), + FamilyName: "updatedFamilyName", + GivenName: "John", + MiddleName: Ptr("X"), + }, + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "john@email.com", + Type: "work", + Primary: true, + }}, + Roles: []*SCIMEnterpriseUserRole{{ + Value: "User", + Primary: Ptr(false), + }}, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseAttribute{ + Schemas: []string{SCIMSchemasURINamespacesPatchOp}, + Operations: []*SCIMEnterpriseAttributeOperation{{ + Op: "replace", + Path: Ptr("emails[type eq 'work'].value"), + Value: Ptr("updatedEmail@email.com"), + }, { + Op: "replace", + Path: Ptr("name.familyName"), + Value: Ptr("updatedFamilyName"), + }}, + } + got, _, err := client.Enterprise.UpdateSCIMUserAttribute(ctx, "ee", "7fce", input) + if err != nil { + t.Fatalf("Enterprise.UpdateSCIMUserAttribute returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Enterprise.UpdateSCIMUserAttribute diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "UpdateSCIMUserAttribute" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.UpdateSCIMUserAttribute(ctx, "\n", "\n", SCIMEnterpriseAttribute{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.UpdateSCIMUserAttribute(ctx, "ee", "7fce", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err }) } diff --git a/github/github-accessors.go b/github/github-accessors.go index bef89aad127..bb937fc4ab6 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -26230,6 +26230,22 @@ func (s *ScanningAnalysis) GetWarning() string { return *s.Warning } +// GetPath returns the Path field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseAttributeOperation) GetPath() string { + if s == nil || s.Path == nil { + return "" + } + return *s.Path +} + +// GetValue returns the Value field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseAttributeOperation) GetValue() string { + if s == nil || s.Value == nil { + return "" + } + return *s.Value +} + // GetDisplay returns the Display field if it's non-nil, zero value otherwise. func (s *SCIMEnterpriseDisplayReference) GetDisplay() string { if s == nil || s.Display == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index f2dab20e030..76059c647c6 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -33848,6 +33848,28 @@ func TestScanningAnalysis_GetWarning(tt *testing.T) { s.GetWarning() } +func TestSCIMEnterpriseAttributeOperation_GetPath(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseAttributeOperation{Path: &zeroValue} + s.GetPath() + s = &SCIMEnterpriseAttributeOperation{} + s.GetPath() + s = nil + s.GetPath() +} + +func TestSCIMEnterpriseAttributeOperation_GetValue(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseAttributeOperation{Value: &zeroValue} + s.GetValue() + s = &SCIMEnterpriseAttributeOperation{} + s.GetValue() + s = nil + s.GetValue() +} + func TestSCIMEnterpriseDisplayReference_GetDisplay(tt *testing.T) { tt.Parallel() var zeroValue string From 841abae1cfd34580fa83ad32ef67a90d562efadd Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 4 Dec 2025 13:09:01 +0000 Subject: [PATCH 16/49] chore: Update golangci-lint to v2.7.0 (#3853) --- .custom-gcl.yml | 2 +- .gitignore | 1 + script/lint.sh | 4 ++-- tools/gen-release-notes/main.go | 8 ++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.custom-gcl.yml b/.custom-gcl.yml index a0a0945e11a..139e6574ed4 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -1,4 +1,4 @@ -version: v2.6.1 +version: v2.7.0 plugins: - module: "github.com/google/go-github/v79/tools/fmtpercentv" path: ./tools/fmtpercentv diff --git a/.gitignore b/.gitignore index 985f2550f5b..6b918ae17ff 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ vendor/ # golangci-lint -v custom generates the following local file: custom-gcl +custom-gcl.exe diff --git a/script/lint.sh b/script/lint.sh index fc077cbaf24..ba9fd89a648 100755 --- a/script/lint.sh +++ b/script/lint.sh @@ -5,7 +5,7 @@ set -e -GOLANGCI_LINT_VERSION="2.6.1" +GOLANGCI_LINT_VERSION="2.7.0" CDPATH="" cd -- "$(dirname -- "$0")/.." BIN="$(pwd -P)"/bin @@ -22,7 +22,7 @@ fail() { # install golangci-lint and custom-gcl in ./bin if they don't exist with the correct version if ! "$BIN"/custom-gcl --version 2> /dev/null | grep -q "$GOLANGCI_LINT_VERSION"; then GOBIN="$BIN" go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v$GOLANGCI_LINT_VERSION" - "$BIN"/golangci-lint custom && mv ./custom-gcl "$BIN" + "$BIN"/golangci-lint custom --name custom-gcl --destination "$BIN" fi MOD_DIRS="$(git ls-files '*go.mod' | xargs dirname | sort)" diff --git a/tools/gen-release-notes/main.go b/tools/gen-release-notes/main.go index bb25a0c8deb..d02746eb739 100644 --- a/tools/gen-release-notes/main.go +++ b/tools/gen-release-notes/main.go @@ -96,8 +96,8 @@ func splitIntoPRs(text string) []string { func stripPRHTML(text string) string { _, innerText := getTagSequence(text) - if i := strings.Index(text, ""); i > 0 { - newText := text[:i] + strings.Join(innerText, "") + if before, _, ok := strings.Cut(text, ""); ok { + newText := before + strings.Join(innerText, "") newText = strings.ReplaceAll(newText, "…", "") newText = newlinesRE.ReplaceAllString(newText, "\n ") return newText @@ -138,8 +138,8 @@ func getTagSequence(text string) (tagSeq, innerText []string) { tagSeq = append(tagSeq, s) continue } - if i := strings.Index(s, " "); i > 0 { - tagSeq = append(tagSeq, s[0:i]) + if before, _, ok := strings.Cut(s, " "); ok { + tagSeq = append(tagSeq, before) } else { tagSeq = append(tagSeq, s) } From e119eb8f7a88d965a09656aea3fd4977e40de758 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 4 Dec 2025 13:44:20 +0000 Subject: [PATCH 17/49] feat: Add repository target to ruleset (#3850) --- github/github-accessors.go | 40 +++++++ github/github-accessors_test.go | 40 +++++++ github/rules.go | 192 +++++++++++++++++++++++++++----- github/rules_test.go | 51 ++++++++- 4 files changed, 295 insertions(+), 28 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index bb937fc4ab6..76af1d2d588 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -25094,6 +25094,46 @@ func (r *RepositoryRulesetRules) GetPullRequest() *PullRequestRuleParameters { return r.PullRequest } +// GetRepositoryCreate returns the RepositoryCreate field. +func (r *RepositoryRulesetRules) GetRepositoryCreate() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryCreate +} + +// GetRepositoryDelete returns the RepositoryDelete field. +func (r *RepositoryRulesetRules) GetRepositoryDelete() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryDelete +} + +// GetRepositoryName returns the RepositoryName field. +func (r *RepositoryRulesetRules) GetRepositoryName() *SimplePatternRuleParameters { + if r == nil { + return nil + } + return r.RepositoryName +} + +// GetRepositoryTransfer returns the RepositoryTransfer field. +func (r *RepositoryRulesetRules) GetRepositoryTransfer() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryTransfer +} + +// GetRepositoryVisibility returns the RepositoryVisibility field. +func (r *RepositoryRulesetRules) GetRepositoryVisibility() *RepositoryVisibilityRuleParameters { + if r == nil { + return nil + } + return r.RepositoryVisibility +} + // GetRequiredDeployments returns the RequiredDeployments field. func (r *RepositoryRulesetRules) GetRequiredDeployments() *RequiredDeploymentsRuleParameters { if r == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 76059c647c6..937aff97dc9 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -32370,6 +32370,46 @@ func TestRepositoryRulesetRules_GetPullRequest(tt *testing.T) { r.GetPullRequest() } +func TestRepositoryRulesetRules_GetRepositoryCreate(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryCreate() + r = nil + r.GetRepositoryCreate() +} + +func TestRepositoryRulesetRules_GetRepositoryDelete(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryDelete() + r = nil + r.GetRepositoryDelete() +} + +func TestRepositoryRulesetRules_GetRepositoryName(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryName() + r = nil + r.GetRepositoryName() +} + +func TestRepositoryRulesetRules_GetRepositoryTransfer(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryTransfer() + r = nil + r.GetRepositoryTransfer() +} + +func TestRepositoryRulesetRules_GetRepositoryVisibility(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryVisibility() + r = nil + r.GetRepositoryVisibility() +} + func TestRepositoryRulesetRules_GetRequiredDeployments(tt *testing.T) { tt.Parallel() r := &RepositoryRulesetRules{} diff --git a/github/rules.go b/github/rules.go index 9c0df3192d0..2326ed0d7ac 100644 --- a/github/rules.go +++ b/github/rules.go @@ -7,7 +7,7 @@ package github import ( "encoding/json" - "reflect" + "fmt" ) // RulesetTarget represents a GitHub ruleset target. @@ -15,9 +15,10 @@ type RulesetTarget string // This is the set of GitHub ruleset targets. const ( - RulesetTargetBranch RulesetTarget = "branch" - RulesetTargetTag RulesetTarget = "tag" - RulesetTargetPush RulesetTarget = "push" + RulesetTargetBranch RulesetTarget = "branch" + RulesetTargetTag RulesetTarget = "tag" + RulesetTargetPush RulesetTarget = "push" + RulesetTargetRepository RulesetTarget = "repository" ) // RulesetSourceType represents a GitHub ruleset source type. @@ -68,27 +69,37 @@ type RepositoryRuleType string // This is the set of GitHub ruleset rule types. const ( + // Branch or tag target rules. + RulesetRuleTypeBranchNamePattern RepositoryRuleType = "branch_name_pattern" + RulesetRuleTypeCodeScanning RepositoryRuleType = "code_scanning" + RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" + RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" + RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" RulesetRuleTypeCreation RepositoryRuleType = "creation" - RulesetRuleTypeUpdate RepositoryRuleType = "update" RulesetRuleTypeDeletion RepositoryRuleType = "deletion" - RulesetRuleTypeRequiredLinearHistory RepositoryRuleType = "required_linear_history" RulesetRuleTypeMergeQueue RepositoryRuleType = "merge_queue" + RulesetRuleTypeNonFastForward RepositoryRuleType = "non_fast_forward" + RulesetRuleTypePullRequest RepositoryRuleType = "pull_request" RulesetRuleTypeRequiredDeployments RepositoryRuleType = "required_deployments" + RulesetRuleTypeRequiredLinearHistory RepositoryRuleType = "required_linear_history" RulesetRuleTypeRequiredSignatures RepositoryRuleType = "required_signatures" - RulesetRuleTypePullRequest RepositoryRuleType = "pull_request" RulesetRuleTypeRequiredStatusChecks RepositoryRuleType = "required_status_checks" - RulesetRuleTypeNonFastForward RepositoryRuleType = "non_fast_forward" - RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" - RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" - RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" - RulesetRuleTypeBranchNamePattern RepositoryRuleType = "branch_name_pattern" RulesetRuleTypeTagNamePattern RepositoryRuleType = "tag_name_pattern" + RulesetRuleTypeUpdate RepositoryRuleType = "update" + RulesetRuleTypeWorkflows RepositoryRuleType = "workflows" + + // Push target rules. + RulesetRuleTypeFileExtensionRestriction RepositoryRuleType = "file_extension_restriction" RulesetRuleTypeFilePathRestriction RepositoryRuleType = "file_path_restriction" RulesetRuleTypeMaxFilePathLength RepositoryRuleType = "max_file_path_length" - RulesetRuleTypeFileExtensionRestriction RepositoryRuleType = "file_extension_restriction" RulesetRuleTypeMaxFileSize RepositoryRuleType = "max_file_size" - RulesetRuleTypeWorkflows RepositoryRuleType = "workflows" - RulesetRuleTypeCodeScanning RepositoryRuleType = "code_scanning" + + // Repository target rules. + RulesetRuleTypeRepositoryCreate RepositoryRuleType = "repository_create" + RulesetRuleTypeRepositoryDelete RepositoryRuleType = "repository_delete" + RulesetRuleTypeRepositoryName RepositoryRuleType = "repository_name" + RulesetRuleTypeRepositoryTransfer RepositoryRuleType = "repository_transfer" + RulesetRuleTypeRepositoryVisibility RepositoryRuleType = "repository_visibility" ) // MergeGroupingStrategy models a GitHub merge grouping strategy. @@ -277,6 +288,7 @@ type RepositoryRule struct { // RepositoryRulesetRules represents a GitHub ruleset rules object. // This type doesn't have JSON annotations as it uses custom marshaling. type RepositoryRulesetRules struct { + // Branch or tag target rules. Creation *EmptyRuleParameters Update *UpdateRuleParameters Deletion *EmptyRuleParameters @@ -292,17 +304,27 @@ type RepositoryRulesetRules struct { CommitterEmailPattern *PatternRuleParameters BranchNamePattern *PatternRuleParameters TagNamePattern *PatternRuleParameters + Workflows *WorkflowsRuleParameters + CodeScanning *CodeScanningRuleParameters + + // Push target rules. + FileExtensionRestriction *FileExtensionRestrictionRuleParameters FilePathRestriction *FilePathRestrictionRuleParameters MaxFilePathLength *MaxFilePathLengthRuleParameters - FileExtensionRestriction *FileExtensionRestrictionRuleParameters MaxFileSize *MaxFileSizeRuleParameters - Workflows *WorkflowsRuleParameters - CodeScanning *CodeScanningRuleParameters + + // Repository target rules. + RepositoryCreate *EmptyRuleParameters + RepositoryDelete *EmptyRuleParameters + RepositoryName *SimplePatternRuleParameters + RepositoryTransfer *EmptyRuleParameters + RepositoryVisibility *RepositoryVisibilityRuleParameters } // BranchRules represents the rules active for a GitHub repository branch. // This type doesn't have JSON annotations as it uses custom marshaling. type BranchRules struct { + // Branch or tag target rules. Creation []*BranchRuleMetadata Update []*UpdateBranchRule Deletion []*BranchRuleMetadata @@ -318,12 +340,14 @@ type BranchRules struct { CommitterEmailPattern []*PatternBranchRule BranchNamePattern []*PatternBranchRule TagNamePattern []*PatternBranchRule + Workflows []*WorkflowsBranchRule + CodeScanning []*CodeScanningBranchRule + + // Push target rules. + FileExtensionRestriction []*FileExtensionRestrictionBranchRule FilePathRestriction []*FilePathRestrictionBranchRule MaxFilePathLength []*MaxFilePathLengthBranchRule - FileExtensionRestriction []*FileExtensionRestrictionBranchRule MaxFileSize []*MaxFileSizeBranchRule - Workflows []*WorkflowsBranchRule - CodeScanning []*CodeScanningBranchRule } // BranchRuleMetadata represents the metadata for a branch rule. @@ -522,6 +546,18 @@ type RuleCodeScanningTool struct { Tool string `json:"tool"` } +// SimplePatternRuleParameters represents the parameters for a simple pattern rule. +type SimplePatternRuleParameters struct { + Negate bool `json:"negate"` + Pattern string `json:"pattern"` +} + +// RepositoryVisibilityRuleParameters represents the repository visibility rule parameters. +type RepositoryVisibilityRuleParameters struct { + Internal bool `json:"internal"` + Private bool `json:"private"` +} + // repositoryRulesetRuleWrapper is a helper type to marshal & unmarshal a ruleset rule. type repositoryRulesetRuleWrapper struct { Type RepositoryRuleType `json:"type"` @@ -702,17 +738,74 @@ func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { rawRules = append(rawRules, json.RawMessage(bytes)) } + if r.RepositoryCreate != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryCreate, r.RepositoryCreate) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryDelete != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryDelete, r.RepositoryDelete) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryName != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryName, r.RepositoryName) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryTransfer != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryTransfer, r.RepositoryTransfer) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryVisibility != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryVisibility, r.RepositoryVisibility) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + return json.Marshal(rawRules) } // marshalRepositoryRulesetRule is a helper function to marshal a ruleset rule. -// -// TODO: Benchmark the code that uses reflection. -// TODO: Use a generator here instead of reflection if there is a significant performance hit. func marshalRepositoryRulesetRule[T any](t RepositoryRuleType, params T) ([]byte, error) { - paramsType := reflect.TypeFor[T]() + hasParams := true + + switch t { + case RulesetRuleTypeCreation, + RulesetRuleTypeDeletion, + RulesetRuleTypeRequiredLinearHistory, + RulesetRuleTypeRequiredSignatures, + RulesetRuleTypeNonFastForward, + RulesetRuleTypeRepositoryCreate, + RulesetRuleTypeRepositoryDelete, + RulesetRuleTypeRepositoryTransfer: + hasParams = false + case RulesetRuleTypeUpdate: + paramsTyped, ok := any(params).(*UpdateRuleParameters) + if !ok { + return nil, fmt.Errorf("expected UpdateRuleParameters for rule type %v", t) + } + if paramsTyped == nil || *paramsTyped == (UpdateRuleParameters{}) { + hasParams = false + } + } - if paramsType.Kind() == reflect.Pointer && (reflect.ValueOf(params).IsNil() || reflect.ValueOf(params).Elem().IsZero()) { + if !hasParams { return json.Marshal(repositoryRulesetRuleWrapper{Type: t}) } @@ -872,6 +965,28 @@ func (r *RepositoryRulesetRules) UnmarshalJSON(data []byte) error { return err } } + case RulesetRuleTypeRepositoryCreate: + r.RepositoryCreate = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryDelete: + r.RepositoryDelete = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryName: + r.RepositoryName = &SimplePatternRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.RepositoryName); err != nil { + return err + } + } + case RulesetRuleTypeRepositoryTransfer: + r.RepositoryTransfer = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryVisibility: + r.RepositoryVisibility = &RepositoryVisibilityRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.RepositoryVisibility); err != nil { + return err + } + } } } @@ -1251,6 +1366,31 @@ func (r *RepositoryRule) UnmarshalJSON(data []byte) error { } } + r.Parameters = p + case RulesetRuleTypeRepositoryCreate: + r.Parameters = nil + case RulesetRuleTypeRepositoryDelete: + r.Parameters = nil + case RulesetRuleTypeRepositoryName: + p := &SimplePatternRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } + + r.Parameters = p + case RulesetRuleTypeRepositoryTransfer: + r.Parameters = nil + case RulesetRuleTypeRepositoryVisibility: + p := &RepositoryVisibilityRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } r.Parameters = p } diff --git a/github/rules_test.go b/github/rules_test.go index 562d385ec74..cd0f2ae2377 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -122,8 +122,13 @@ func TestRulesetRules(t *testing.T) { }, }, }, + RepositoryCreate: &EmptyRuleParameters{}, + RepositoryDelete: &EmptyRuleParameters{}, + RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, + RepositoryTransfer: &EmptyRuleParameters{}, + RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}]`, + `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, { "all_rules_with_all_params", @@ -235,8 +240,13 @@ func TestRulesetRules(t *testing.T) { }, }, }, + RepositoryCreate: &EmptyRuleParameters{}, + RepositoryDelete: &EmptyRuleParameters{}, + RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, + RepositoryTransfer: &EmptyRuleParameters{}, + RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}]`, + `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, } @@ -919,6 +929,43 @@ func TestRepositoryRule(t *testing.T) { }, `{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}`, }, + { + "repository_create", + &RepositoryRule{Type: RulesetRuleTypeRepositoryCreate, Parameters: nil}, + `{"type":"repository_create"}`, + }, + { + "repository_delete", + &RepositoryRule{Type: RulesetRuleTypeRepositoryDelete, Parameters: nil}, + `{"type":"repository_delete"}`, + }, + { + "repository_name", + &RepositoryRule{ + Type: RulesetRuleTypeRepositoryName, + Parameters: &SimplePatternRuleParameters{ + Negate: false, + Pattern: "^test-.+", + }, + }, + `{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}}`, + }, + { + "repository_transfer", + &RepositoryRule{Type: RulesetRuleTypeRepositoryTransfer, Parameters: nil}, + `{"type":"repository_transfer"}`, + }, + { + "repository_visibility", + &RepositoryRule{ + Type: RulesetRuleTypeRepositoryVisibility, + Parameters: &RepositoryVisibilityRuleParameters{ + Internal: false, + Private: false, + }, + }, + `{"type":"repository_visibility","parameters":{"internal":false,"private":false}}`, + }, } t.Run("UnmarshalJSON", func(t *testing.T) { From bea9884b07cc33a0d921ce7416fdb6fb6952e6a8 Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:41:29 -0500 Subject: [PATCH 18/49] Bump version of go-github to v80.0.0 (#3854) --- .custom-gcl.yml | 6 +++--- .golangci.yml | 6 +++--- README.md | 20 +++++++++---------- example/actionpermissions/main.go | 2 +- example/appengine/app.go | 2 +- example/basicauth/main.go | 2 +- .../newreposecretwithxcrypto/main.go | 2 +- .../newusersecretwithxcrypto/main.go | 2 +- example/commitpr/main.go | 6 +++--- example/go.mod | 6 +++--- example/listenvironments/main.go | 2 +- example/migrations/main.go | 2 +- example/newfilewithappauth/main.go | 2 +- example/newrepo/main.go | 2 +- example/newreposecretwithlibsodium/go.mod | 4 ++-- example/newreposecretwithlibsodium/main.go | 2 +- example/newreposecretwithxcrypto/main.go | 2 +- example/ratelimit/main.go | 2 +- example/simple/main.go | 2 +- example/tokenauth/main.go | 2 +- example/topics/main.go | 2 +- example/verifyartifact/main.go | 2 +- github/doc.go | 2 +- github/examples_test.go | 2 +- github/github.go | 2 +- go.mod | 2 +- test/fields/fields.go | 2 +- test/integration/activity_test.go | 2 +- test/integration/authorizations_test.go | 2 +- test/integration/github_test.go | 2 +- test/integration/projects_test.go | 2 +- test/integration/repos_test.go | 2 +- test/integration/users_test.go | 2 +- tools/go.mod | 4 ++-- tools/metadata/main.go | 2 +- tools/metadata/main_test.go | 2 +- tools/metadata/metadata.go | 2 +- tools/metadata/openapi.go | 2 +- 38 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.custom-gcl.yml b/.custom-gcl.yml index 139e6574ed4..155c3d17601 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -1,8 +1,8 @@ version: v2.7.0 plugins: - - module: "github.com/google/go-github/v79/tools/fmtpercentv" + - module: "github.com/google/go-github/v80/tools/fmtpercentv" path: ./tools/fmtpercentv - - module: "github.com/google/go-github/v79/tools/sliceofpointers" + - module: "github.com/google/go-github/v80/tools/sliceofpointers" path: ./tools/sliceofpointers - - module: "github.com/google/go-github/v79/tools/structfield" + - module: "github.com/google/go-github/v80/tools/structfield" path: ./tools/structfield diff --git a/.golangci.yml b/.golangci.yml index df503f41577..bfd04310643 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -151,15 +151,15 @@ linters: fmtpercentv: type: module description: Reports usage of %d or %s in format strings. - original-url: github.com/google/go-github/v79/tools/fmtpercentv + original-url: github.com/google/go-github/v80/tools/fmtpercentv sliceofpointers: type: module description: Reports usage of []*string and slices of structs without pointers. - original-url: github.com/google/go-github/v79/tools/sliceofpointers + original-url: github.com/google/go-github/v80/tools/sliceofpointers structfield: type: module description: Reports mismatches between Go field and JSON, URL tag names and types. - original-url: github.com/google/go-github/v79/tools/structfield + original-url: github.com/google/go-github/v80/tools/structfield settings: allowed-tag-names: - ActionsCacheUsageList.RepoCacheUsage # TODO: RepoCacheUsages ? diff --git a/README.md b/README.md index 8862fa2ebab..5e66d0a2c94 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # go-github # [![go-github release (latest SemVer)](https://img.shields.io/github/v/release/google/go-github?sort=semver)](https://github.com/google/go-github/releases) -[![Go Reference](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/google/go-github/v79/github) +[![Go Reference](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/google/go-github/v80/github) [![Test Status](https://github.com/google/go-github/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/google/go-github/actions/workflows/tests.yml) [![Test Coverage](https://codecov.io/gh/google/go-github/branch/master/graph/badge.svg)](https://codecov.io/gh/google/go-github) [![Discuss at go-github@googlegroups.com](https://img.shields.io/badge/discuss-go--github%40googlegroups.com-blue.svg)](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/v79 +go get github.com/google/go-github/v80 ``` 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/v79/github" +import "github.com/google/go-github/v80/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/v79@master +go get github.com/google/go-github/v80@master ``` ## Usage ## ```go -import "github.com/google/go-github/v79/github" // with go modules enabled (GO111MODULE=on or outside GOPATH) +import "github.com/google/go-github/v80/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/v79/github#BasicAuthTransport). +[`BasicAuthTransport`](https://pkg.go.dev/github.com/google/go-github/v80/github#BasicAuthTransport). #### As a GitHub App #### @@ -125,7 +125,7 @@ import ( "net/http" "github.com/bradleyfalzon/ghinstallation/v2" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func main() { @@ -159,7 +159,7 @@ import ( "os" "strconv" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/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/v79/github +[package docs]: https://pkg.go.dev/github.com/google/go-github/v80/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 | | ----------------- | --------------------- | -| 79.0.0 | 2022-11-28 | +| 80.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 87a5cad8e5f..eb66b57353e 100644 --- a/example/actionpermissions/main.go +++ b/example/actionpermissions/main.go @@ -14,7 +14,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) var ( diff --git a/example/appengine/app.go b/example/appengine/app.go index 4d1de1c444a..f8bc85fed4e 100644 --- a/example/appengine/app.go +++ b/example/appengine/app.go @@ -12,7 +12,7 @@ import ( "net/http" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "google.golang.org/appengine" "google.golang.org/appengine/log" ) diff --git a/example/basicauth/main.go b/example/basicauth/main.go index e468407a866..0af21681288 100644 --- a/example/basicauth/main.go +++ b/example/basicauth/main.go @@ -22,7 +22,7 @@ import ( "os" "strings" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/term" ) diff --git a/example/codespaces/newreposecretwithxcrypto/main.go b/example/codespaces/newreposecretwithxcrypto/main.go index 6fe62b7653f..b481a1fa431 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/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/crypto/nacl/box" ) diff --git a/example/codespaces/newusersecretwithxcrypto/main.go b/example/codespaces/newusersecretwithxcrypto/main.go index 8b94b8db5fd..26d5615792f 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/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/crypto/nacl/box" ) diff --git a/example/commitpr/main.go b/example/commitpr/main.go index 3af1c289ad8..061b4ed93ed 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/v79/github#example-RepositoriesService-CreateFile +// https://pkg.go.dev/github.com/google/go-github/v80/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/v79/github" + "github.com/google/go-github/v80/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/v79/github#example-PullRequestsService-Create +// createPR creates a pull request. Based on: https://pkg.go.dev/github.com/google/go-github/v80/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 96ac958d2a8..9c3170e1fbb 100644 --- a/example/go.mod +++ b/example/go.mod @@ -1,4 +1,4 @@ -module github.com/google/go-github/v79/example +module github.com/google/go-github/v80/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/v79 v79.0.0 + github.com/google/go-github/v80 v80.0.0 github.com/sigstore/sigstore-go v0.6.1 golang.org/x/crypto v0.45.0 golang.org/x/term v0.37.0 @@ -100,4 +100,4 @@ require ( ) // Use version at HEAD, not the latest published. -replace github.com/google/go-github/v79 => ../ +replace github.com/google/go-github/v80 => ../ diff --git a/example/listenvironments/main.go b/example/listenvironments/main.go index 31629a52a93..87d277eaa36 100644 --- a/example/listenvironments/main.go +++ b/example/listenvironments/main.go @@ -18,7 +18,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func main() { diff --git a/example/migrations/main.go b/example/migrations/main.go index 2afc3635380..62ca19724c5 100644 --- a/example/migrations/main.go +++ b/example/migrations/main.go @@ -12,7 +12,7 @@ import ( "context" "fmt" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func fetchAllUserMigrations() ([]*github.UserMigration, error) { diff --git a/example/newfilewithappauth/main.go b/example/newfilewithappauth/main.go index 6de89c426ac..43cbf1750a8 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/v79/github" + "github.com/google/go-github/v80/github" ) func main() { diff --git a/example/newrepo/main.go b/example/newrepo/main.go index 939e7de2747..c47d3d7c181 100644 --- a/example/newrepo/main.go +++ b/example/newrepo/main.go @@ -16,7 +16,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) var ( diff --git a/example/newreposecretwithlibsodium/go.mod b/example/newreposecretwithlibsodium/go.mod index f2960042c91..6741658305a 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/v79 v79.0.0 + github.com/google/go-github/v80 v80.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/v79 => ../.. +replace github.com/google/go-github/v80 => ../.. diff --git a/example/newreposecretwithlibsodium/main.go b/example/newreposecretwithlibsodium/main.go index de8010a3c34..dbbecab8d63 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/v79/github" + "github.com/google/go-github/v80/github" ) var ( diff --git a/example/newreposecretwithxcrypto/main.go b/example/newreposecretwithxcrypto/main.go index 9b67d793364..eff471f0dbf 100644 --- a/example/newreposecretwithxcrypto/main.go +++ b/example/newreposecretwithxcrypto/main.go @@ -37,7 +37,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/crypto/nacl/box" ) diff --git a/example/ratelimit/main.go b/example/ratelimit/main.go index 29db85ca3ff..7d671215bb6 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/v79/github" + "github.com/google/go-github/v80/github" ) func main() { diff --git a/example/simple/main.go b/example/simple/main.go index 93e25a3aefb..da51beea1d3 100644 --- a/example/simple/main.go +++ b/example/simple/main.go @@ -12,7 +12,7 @@ import ( "context" "fmt" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) // Fetch all the public organizations' membership of a user. diff --git a/example/tokenauth/main.go b/example/tokenauth/main.go index b1348a123d9..32e1b90a7f8 100644 --- a/example/tokenauth/main.go +++ b/example/tokenauth/main.go @@ -15,7 +15,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/term" ) diff --git a/example/topics/main.go b/example/topics/main.go index 276e8693c89..bedb6e03c2d 100644 --- a/example/topics/main.go +++ b/example/topics/main.go @@ -12,7 +12,7 @@ import ( "context" "fmt" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/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 0ad86cfc8b6..2895602bd4c 100644 --- a/example/verifyartifact/main.go +++ b/example/verifyartifact/main.go @@ -18,7 +18,7 @@ import ( "log" "os" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/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 a9fcb8dacb5..ef3a5bb236f 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/v79/github" // with go modules enabled (GO111MODULE=on or outside GOPATH) + import "github.com/google/go-github/v80/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 30674d0072a..43f38384b78 100644 --- a/github/examples_test.go +++ b/github/examples_test.go @@ -12,7 +12,7 @@ import ( "fmt" "log" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func ExampleMarkdownService_Render() { diff --git a/github/github.go b/github/github.go index b9efa3b4e89..98213e1b900 100644 --- a/github/github.go +++ b/github/github.go @@ -29,7 +29,7 @@ import ( ) const ( - Version = "v79.0.0" + Version = "v80.0.0" defaultAPIVersion = "2022-11-28" defaultBaseURL = "https://api.github.com/" diff --git a/go.mod b/go.mod index 31a0631c6e2..2b7a09f3c62 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/google/go-github/v79 +module github.com/google/go-github/v80 go 1.24.0 diff --git a/test/fields/fields.go b/test/fields/fields.go index adcbad9fd84..afc1724941f 100644 --- a/test/fields/fields.go +++ b/test/fields/fields.go @@ -25,7 +25,7 @@ import ( "reflect" "strings" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) var ( diff --git a/test/integration/activity_test.go b/test/integration/activity_test.go index 93b6c563ddd..7aea2583c6f 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/v79/github" + "github.com/google/go-github/v80/github" ) const ( diff --git a/test/integration/authorizations_test.go b/test/integration/authorizations_test.go index 4f876b3effd..9b0482f6f0d 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/v79/github" + "github.com/google/go-github/v80/github" ) const ( diff --git a/test/integration/github_test.go b/test/integration/github_test.go index abb4555afe1..30be678079b 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/v79/github" + "github.com/google/go-github/v80/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 c8b2265e72b..eb9ea366b06 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/v79/github" + "github.com/google/go-github/v80/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 e5a2cb0cc11..0b4d82a3a6b 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/v79/github" + "github.com/google/go-github/v80/github" ) func TestRepositories_CRUD(t *testing.T) { diff --git a/test/integration/users_test.go b/test/integration/users_test.go index 5d6354ca077..8eefd2fac3b 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/v79/github" + "github.com/google/go-github/v80/github" ) func TestUsers_Get(t *testing.T) { diff --git a/tools/go.mod b/tools/go.mod index ecbed2b9ab5..e51b910a93e 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -6,7 +6,7 @@ require ( github.com/alecthomas/kong v1.13.0 github.com/getkin/kin-openapi v0.133.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v79 v79.0.0 + github.com/google/go-github/v80 v80.0.0 golang.org/x/sync v0.18.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -26,4 +26,4 @@ require ( ) // Use version at HEAD, not the latest published. -replace github.com/google/go-github/v79 => ../ +replace github.com/google/go-github/v80 => ../ diff --git a/tools/metadata/main.go b/tools/metadata/main.go index fcc4e7633e5..6c9859945fe 100644 --- a/tools/metadata/main.go +++ b/tools/metadata/main.go @@ -16,7 +16,7 @@ import ( "path/filepath" "github.com/alecthomas/kong" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) var helpVars = kong.Vars{ diff --git a/tools/metadata/main_test.go b/tools/metadata/main_test.go index 8ac3dea216c..2a75b495ade 100644 --- a/tools/metadata/main_test.go +++ b/tools/metadata/main_test.go @@ -23,7 +23,7 @@ import ( "github.com/alecthomas/kong" "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func TestUpdateGo(t *testing.T) { diff --git a/tools/metadata/metadata.go b/tools/metadata/metadata.go index 1050e598e61..3c5bc406ee9 100644 --- a/tools/metadata/metadata.go +++ b/tools/metadata/metadata.go @@ -24,7 +24,7 @@ import ( "strings" "sync" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "gopkg.in/yaml.v3" ) diff --git a/tools/metadata/openapi.go b/tools/metadata/openapi.go index 5205a53cbe7..011a1aea98e 100644 --- a/tools/metadata/openapi.go +++ b/tools/metadata/openapi.go @@ -14,7 +14,7 @@ import ( "strconv" "github.com/getkin/kin-openapi/openapi3" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" "golang.org/x/sync/errgroup" ) From 66f826ccc53b93b096649efc0a7c93a06cb3e955 Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:56:25 -0500 Subject: [PATCH 19/49] Bump go-github from v79 to v80 in /scrape (#3855) --- scrape/apps.go | 2 +- scrape/apps_test.go | 2 +- scrape/go.mod | 2 +- scrape/go.sum | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scrape/apps.go b/scrape/apps.go index 3a2e4bc3c52..f221fd2026c 100644 --- a/scrape/apps.go +++ b/scrape/apps.go @@ -18,7 +18,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) // AppRestrictionsEnabled returns whether the specified organization has diff --git a/scrape/apps_test.go b/scrape/apps_test.go index 19987ab4a0e..54756f46c44 100644 --- a/scrape/apps_test.go +++ b/scrape/apps_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-github/v79/github" + "github.com/google/go-github/v80/github" ) func Test_AppRestrictionsEnabled(t *testing.T) { diff --git a/scrape/go.mod b/scrape/go.mod index 66343f26ba4..4a3094bb570 100644 --- a/scrape/go.mod +++ b/scrape/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/PuerkitoBio/goquery v1.11.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-github/v79 v79.0.0 + github.com/google/go-github/v80 v80.0.0 github.com/xlzd/gotp v0.1.0 golang.org/x/net v0.47.0 ) diff --git a/scrape/go.sum b/scrape/go.sum index 720a103822f..50fd096e667 100644 --- a/scrape/go.sum +++ b/scrape/go.sum @@ -6,8 +6,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v79 v79.0.0 h1:MdodQojuFPBhmtwHiBcIGLw/e/wei2PvFX9ndxK0X4Y= -github.com/google/go-github/v79 v79.0.0/go.mod h1:OAFbNhq7fQwohojb06iIIQAB9CBGYLq999myfUFnrS4= +github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs= +github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= From bd1caa090d43cb868fef118c57e73d3815333c7a Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Mon, 8 Dec 2025 17:45:51 +0530 Subject: [PATCH 20/49] feat: Add support for Enterprise Team APIs (#3861) --- github/enterprise_team.go | 152 ++++++++++++++++++++ github/enterprise_team_test.go | 236 ++++++++++++++++++++++++++++++++ github/github-accessors.go | 40 ++++++ github/github-accessors_test.go | 55 ++++++++ 4 files changed, 483 insertions(+) create mode 100644 github/enterprise_team.go create mode 100644 github/enterprise_team_test.go diff --git a/github/enterprise_team.go b/github/enterprise_team.go new file mode 100644 index 00000000000..4cbcb89ed1d --- /dev/null +++ b/github/enterprise_team.go @@ -0,0 +1,152 @@ +// 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" +) + +// EnterpriseTeam represent a team in a GitHub Enterprise. +type EnterpriseTeam struct { + ID int64 `json:"id"` + URL string `json:"url"` + MemberURL string `json:"member_url"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + HTMLURL string `json:"html_url"` + Slug string `json:"slug"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` + GroupID string `json:"group_id"` + OrganizationSelectionType *string `json:"organization_selection_type,omitempty"` +} + +// EnterpriseTeamCreateOrUpdateRequest is used to create or update an enterprise team. +type EnterpriseTeamCreateOrUpdateRequest struct { + // The name of the team. + Name string `json:"name"` + // A description of the team. + Description *string `json:"description,omitempty"` + // Specifies which organizations in the enterprise should have access to this team. + // Possible values are "disabled" , "all" and "selected". If not specified, the default is "disabled". + OrganizationSelectionType *string `json:"organization_selection_type,omitempty"` + // The ID of the IdP group to assign team membership with. + GroupID *string `json:"group_id,omitempty"` +} + +// ListTeams lists all teams in an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#list-enterprise-teams +// +//meta:operation GET /enterprises/{enterprise}/teams +func (s *EnterpriseService) ListTeams(ctx context.Context, enterprise string, opt *ListOptions) ([]*EnterpriseTeam, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams", enterprise) + u, err := addOptions(u, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var teams []*EnterpriseTeam + resp, err := s.client.Do(ctx, req, &teams) + if err != nil { + return nil, resp, err + } + + return teams, resp, nil +} + +// CreateTeam creates a new team in an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#create-an-enterprise-team +// +//meta:operation POST /enterprises/{enterprise}/teams +func (s *EnterpriseService) CreateTeam(ctx context.Context, enterprise string, team EnterpriseTeamCreateOrUpdateRequest) (*EnterpriseTeam, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams", enterprise) + + req, err := s.client.NewRequest("POST", u, team) + if err != nil { + return nil, nil, err + } + + var createdTeam *EnterpriseTeam + resp, err := s.client.Do(ctx, req, &createdTeam) + if err != nil { + return nil, resp, err + } + + return createdTeam, resp, nil +} + +// GetTeam retrieves a team in an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#get-an-enterprise-team +// +//meta:operation GET /enterprises/{enterprise}/teams/{team_slug} +func (s *EnterpriseService) GetTeam(ctx context.Context, enterprise, teamSlug string) (*EnterpriseTeam, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v", enterprise, teamSlug) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var team *EnterpriseTeam + resp, err := s.client.Do(ctx, req, &team) + if err != nil { + return nil, resp, err + } + + return team, resp, nil +} + +// UpdateTeam updates a team in an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#update-an-enterprise-team +// +//meta:operation PATCH /enterprises/{enterprise}/teams/{team_slug} +func (s *EnterpriseService) UpdateTeam(ctx context.Context, enterprise, teamSlug string, team EnterpriseTeamCreateOrUpdateRequest) (*EnterpriseTeam, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v", enterprise, teamSlug) + + req, err := s.client.NewRequest("PATCH", u, team) + if err != nil { + return nil, nil, err + } + + var updatedTeam *EnterpriseTeam + resp, err := s.client.Do(ctx, req, &updatedTeam) + if err != nil { + return nil, resp, err + } + + return updatedTeam, resp, nil +} + +// DeleteTeam deletes a team in an enterprise. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#delete-an-enterprise-team +// +//meta:operation DELETE /enterprises/{enterprise}/teams/{team_slug} +func (s *EnterpriseService) DeleteTeam(ctx context.Context, enterprise, teamSlug string) (*Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v", enterprise, teamSlug) + + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/enterprise_team_test.go b/github/enterprise_team_test.go new file mode 100644 index 00000000000..573b1ec36d1 --- /dev/null +++ b/github/enterprise_team_test.go @@ -0,0 +1,236 @@ +// 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 ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestEnterpriseService_ListTeams(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{ + "id": 1, + "url": "https://example.com/team1", + "member_url": "https://example.com/members", + "name": "Team One", + "html_url": "https://example.com/html", + "slug": "team-one", + "created_at": "2020-01-01T00:00:00Z", + "updated_at": "2020-01-02T00:00:00Z", + "group_id": "99" + }]`) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + got, _, err := client.Enterprise.ListTeams(ctx, "e", opts) + if err != nil { + t.Fatalf("Enterprise.ListTeams returned error: %v", err) + } + + want := []*EnterpriseTeam{ + { + ID: 1, + URL: "https://example.com/team1", + MemberURL: "https://example.com/members", + Name: "Team One", + HTMLURL: "https://example.com/html", + Slug: "team-one", + GroupID: "99", + CreatedAt: Timestamp{Time: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)}, + UpdatedAt: Timestamp{Time: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC)}, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.ListTeams = %+v, want %+v", got, want) + } + + const methodName = "ListTeams" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.ListTeams(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListTeams(ctx, "e", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_CreateTeam(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := EnterpriseTeamCreateOrUpdateRequest{ + Name: "New Team", + } + + mux.HandleFunc("/enterprises/e/teams", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"name":"New Team"}`+"\n") + fmt.Fprint(w, `{ + "id": 10, + "name": "New Team", + "slug": "new-team", + "url": "https://example.com/team" + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.CreateTeam(ctx, "e", input) + if err != nil { + t.Fatalf("Enterprise.CreateTeam returned error: %v", err) + } + + want := &EnterpriseTeam{ + ID: 10, + Name: "New Team", + Slug: "new-team", + URL: "https://example.com/team", + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.CreateTeam = %+v, want %+v", got, want) + } + + const methodName = "CreateTeam" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.CreateTeam(ctx, "e", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_GetTeam(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "id": 2, + "name": "Team One", + "slug": "t1" + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.GetTeam(ctx, "e", "t1") + if err != nil { + t.Fatalf("Enterprise.GetTeam returned error: %v", err) + } + + want := &EnterpriseTeam{ + ID: 2, + Name: "Team One", + Slug: "t1", + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.GetTeam = %+v, want %+v", got, want) + } + + const methodName = "GetTeam" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.GetTeam(ctx, "\n", "t1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetTeam(ctx, "e", "t1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_UpdateTeam(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := EnterpriseTeamCreateOrUpdateRequest{ + Name: "Updated Team", + } + + mux.HandleFunc("/enterprises/e/teams/t1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"name":"Updated Team"}`+"\n") + fmt.Fprint(w, `{ + "id": 3, + "name": "Updated Team", + "slug": "t1" + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.UpdateTeam(ctx, "e", "t1", input) + if err != nil { + t.Fatalf("Enterprise.UpdateTeam returned error: %v", err) + } + + want := &EnterpriseTeam{ + ID: 3, + Name: "Updated Team", + Slug: "t1", + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.UpdateTeam = %+v, want %+v", got, want) + } + + const methodName = "UpdateTeam" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.UpdateTeam(ctx, "\n", "t1", input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.UpdateTeam(ctx, "e", "t1", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_DeleteTeam(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + _, err := client.Enterprise.DeleteTeam(ctx, "e", "t1") + if err != nil { + t.Fatalf("Enterprise.DeleteTeam returned error: %v", err) + } + + const methodName = "DeleteTeam" + testBadOptions(t, methodName, func() error { + _, err := client.Enterprise.DeleteTeam(ctx, "\n", "t1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.DeleteTeam(ctx, "e", "t1") + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 76af1d2d588..fa729b5f565 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -9446,6 +9446,46 @@ func (e *EnterpriseSecurityAnalysisSettings) GetSecretScanningValidityChecksEnab return *e.SecretScanningValidityChecksEnabled } +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (e *EnterpriseTeam) GetDescription() string { + if e == nil || e.Description == nil { + return "" + } + return *e.Description +} + +// GetOrganizationSelectionType returns the OrganizationSelectionType field if it's non-nil, zero value otherwise. +func (e *EnterpriseTeam) GetOrganizationSelectionType() string { + if e == nil || e.OrganizationSelectionType == nil { + return "" + } + return *e.OrganizationSelectionType +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (e *EnterpriseTeamCreateOrUpdateRequest) GetDescription() string { + if e == nil || e.Description == nil { + return "" + } + return *e.Description +} + +// GetGroupID returns the GroupID field if it's non-nil, zero value otherwise. +func (e *EnterpriseTeamCreateOrUpdateRequest) GetGroupID() string { + if e == nil || e.GroupID == nil { + return "" + } + return *e.GroupID +} + +// GetOrganizationSelectionType returns the OrganizationSelectionType field if it's non-nil, zero value otherwise. +func (e *EnterpriseTeamCreateOrUpdateRequest) GetOrganizationSelectionType() string { + if e == nil || e.OrganizationSelectionType == nil { + return "" + } + return *e.OrganizationSelectionType +} + // GetCanAdminsBypass returns the CanAdminsBypass field if it's non-nil, zero value otherwise. func (e *Environment) GetCanAdminsBypass() bool { if e == nil || e.CanAdminsBypass == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 937aff97dc9..ca457c5ce98 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -12246,6 +12246,61 @@ func TestEnterpriseSecurityAnalysisSettings_GetSecretScanningValidityChecksEnabl e.GetSecretScanningValidityChecksEnabled() } +func TestEnterpriseTeam_GetDescription(tt *testing.T) { + tt.Parallel() + var zeroValue string + e := &EnterpriseTeam{Description: &zeroValue} + e.GetDescription() + e = &EnterpriseTeam{} + e.GetDescription() + e = nil + e.GetDescription() +} + +func TestEnterpriseTeam_GetOrganizationSelectionType(tt *testing.T) { + tt.Parallel() + var zeroValue string + e := &EnterpriseTeam{OrganizationSelectionType: &zeroValue} + e.GetOrganizationSelectionType() + e = &EnterpriseTeam{} + e.GetOrganizationSelectionType() + e = nil + e.GetOrganizationSelectionType() +} + +func TestEnterpriseTeamCreateOrUpdateRequest_GetDescription(tt *testing.T) { + tt.Parallel() + var zeroValue string + e := &EnterpriseTeamCreateOrUpdateRequest{Description: &zeroValue} + e.GetDescription() + e = &EnterpriseTeamCreateOrUpdateRequest{} + e.GetDescription() + e = nil + e.GetDescription() +} + +func TestEnterpriseTeamCreateOrUpdateRequest_GetGroupID(tt *testing.T) { + tt.Parallel() + var zeroValue string + e := &EnterpriseTeamCreateOrUpdateRequest{GroupID: &zeroValue} + e.GetGroupID() + e = &EnterpriseTeamCreateOrUpdateRequest{} + e.GetGroupID() + e = nil + e.GetGroupID() +} + +func TestEnterpriseTeamCreateOrUpdateRequest_GetOrganizationSelectionType(tt *testing.T) { + tt.Parallel() + var zeroValue string + e := &EnterpriseTeamCreateOrUpdateRequest{OrganizationSelectionType: &zeroValue} + e.GetOrganizationSelectionType() + e = &EnterpriseTeamCreateOrUpdateRequest{} + e.GetOrganizationSelectionType() + e = nil + e.GetOrganizationSelectionType() +} + func TestEnvironment_GetCanAdminsBypass(tt *testing.T) { tt.Parallel() var zeroValue bool From 6922e3bf5096de0df614e54dbb9582ddf5af0cd7 Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:18:34 +0100 Subject: [PATCH 21/49] feat: Implement Enterprise SCIM - Set Groups or Users (#3858) --- github/enterprise_scim.go | 54 ++++++++++++ github/enterprise_scim_test.go | 149 +++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index 3243841a88c..8407857c11d 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -214,6 +214,60 @@ func (s *EnterpriseService) ListProvisionedSCIMUsers(ctx context.Context, enterp return users, resp, nil } +// SetProvisionedSCIMGroup replaces an existing provisioned group’s information. +// +// You must provide all the information required for the group as if you were provisioning it for the first time. Any +// existing group information that you don't provide will be removed, including group membership. To update only +// specific attributes, refer to the `Enterprise.UpdateSCIMGroupAttribute()` method. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#set-scim-information-for-a-provisioned-enterprise-group +// +//meta:operation PUT /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} +func (s *EnterpriseService) SetProvisionedSCIMGroup(ctx context.Context, enterprise, scimGroupID string, group SCIMEnterpriseGroupAttributes) (*SCIMEnterpriseGroupAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Groups/%v", enterprise, scimGroupID) + req, err := s.client.NewRequest("PUT", u, group) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + groupNew := new(SCIMEnterpriseGroupAttributes) + resp, err := s.client.Do(ctx, req, groupNew) + if err != nil { + return nil, resp, err + } + + return groupNew, resp, nil +} + +// SetProvisionedSCIMUser replaces an existing provisioned user's information. +// +// You must supply complete user information, just as you would when provisioning them initially. Any previously +// existing data not provided will be deleted. To update specific attributes only, refer to the +// `Enterprise.UpdateSCIMUserAttribute()` method. +// +// **Warning**: Setting `active: false` will suspend a user, and their handle and email will be obfuscated. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#set-scim-information-for-a-provisioned-enterprise-user +// +//meta:operation PUT /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} +func (s *EnterpriseService) SetProvisionedSCIMUser(ctx context.Context, enterprise, scimUserID string, user SCIMEnterpriseUserAttributes) (*SCIMEnterpriseUserAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users/%v", enterprise, scimUserID) + req, err := s.client.NewRequest("PUT", u, user) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + userNew := new(SCIMEnterpriseUserAttributes) + resp, err := s.client.Do(ctx, req, userNew) + if err != nil { + return nil, resp, err + } + + return userNew, resp, nil +} + // UpdateSCIMGroupAttribute updates a provisioned group’s individual attributes. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-group diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index 4f65c27ff71..51bb80eb64e 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -479,6 +479,155 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { }) } +func TestEnterpriseService_SetProvisionedSCIMGroup(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Groups/abcd", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"displayName":"dn","externalId":"8aa1","schemas":["`+SCIMSchemasURINamespacesGroups+`"]}`+"\n") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesGroups+`"], + "id": "abcd", + "externalId": "8aa1", + "displayName": "dn", + "meta": { + "resourceType": "Group", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd" + } + }`) + }) + want := &SCIMEnterpriseGroupAttributes{ + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ID: Ptr("abcd"), + ExternalID: Ptr("8aa1"), + DisplayName: Ptr("dn"), + Meta: &SCIMEnterpriseMeta{ + ResourceType: "Group", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseGroupAttributes{ + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ExternalID: Ptr("8aa1"), + DisplayName: Ptr("dn"), + } + got, _, err := client.Enterprise.SetProvisionedSCIMGroup(ctx, "ee", "abcd", input) + if err != nil { + t.Fatalf("Enterprise.SetProvisionedSCIMGroup returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.SetProvisionedSCIMGroup diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "SetProvisionedSCIMGroup" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.SetProvisionedSCIMGroup(ctx, "\n", "\n", SCIMEnterpriseGroupAttributes{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.SetProvisionedSCIMGroup(ctx, "ee", "abcd", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_SetProvisionedSCIMUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Users/7fce", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"displayName":"John Doe","userName":"e123","emails":[{"value":"john@email.com","primary":true,"type":"work"}],"externalId":"e123","active":true,"schemas":["`+SCIMSchemasURINamespacesUser+`"]}`+"\n") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesUser+`"], + "id": "7fce", + "externalId": "e123", + "active": true, + "userName": "e123", + "displayName": "John Doe", + "emails": [{ + "value": "john@email.com", + "type": "work", + "primary": true + }], + "meta": { + "resourceType": "User", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce" + } + }`) + }) + want := &SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ID: Ptr("7fce"), + ExternalID: "e123", + Active: true, + UserName: "e123", + DisplayName: "John Doe", + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "john@email.com", + Type: "work", + Primary: true, + }}, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ExternalID: "e123", + Active: true, + UserName: "e123", + DisplayName: "John Doe", + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "john@email.com", + Type: "work", + Primary: true, + }}, + } + got, _, err := client.Enterprise.SetProvisionedSCIMUser(ctx, "ee", "7fce", input) + if err != nil { + t.Fatalf("Enterprise.SetProvisionedSCIMUser returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.SetProvisionedSCIMUser diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "SetProvisionedSCIMUser" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.SetProvisionedSCIMUser(ctx, "\n", "\n", SCIMEnterpriseUserAttributes{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.SetProvisionedSCIMUser(ctx, "ee", "7fce", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) { t.Parallel() client, mux, _ := setup(t) From cae23591692579dd9539494a5b14b059613b4c74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:04:58 -0500 Subject: [PATCH 22/49] build(deps): Bump golang.org/x/term from 0.37.0 to 0.38.0 in /example (#3865) --- example/go.mod | 4 ++-- example/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/go.mod b/example/go.mod index 9c3170e1fbb..89d812429be 100644 --- a/example/go.mod +++ b/example/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-github/v80 v80.0.0 github.com/sigstore/sigstore-go v0.6.1 golang.org/x/crypto v0.45.0 - golang.org/x/term v0.37.0 + golang.org/x/term v0.38.0 google.golang.org/appengine v1.6.8 ) @@ -90,7 +90,7 @@ require ( golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/example/go.sum b/example/go.sum index f14290e4637..cc2b7ccd6bf 100644 --- a/example/go.sum +++ b/example/go.sum @@ -378,12 +378,12 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From c58f59876950095c5bfbb52c916ce02433c9ca8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:14:48 -0500 Subject: [PATCH 23/49] build(deps): Bump golang.org/x/sync from 0.18.0 to 0.19.0 in /tools (#3864) --- tools/go.mod | 2 +- tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index e51b910a93e..b7cbfd6c94f 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -7,7 +7,7 @@ require ( github.com/getkin/kin-openapi v0.133.0 github.com/google/go-cmp v0.7.0 github.com/google/go-github/v80 v80.0.0 - golang.org/x/sync v0.18.0 + golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/tools/go.sum b/tools/go.sum index d543c89d8a6..847fdb04eb7 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -47,8 +47,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From adf078fd961bcdb9af2f849da302cb19ec405593 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:19:50 -0500 Subject: [PATCH 24/49] build(deps): Bump actions/checkout from 6.0.0 to 6.0.1 in the actions group (#3863) --- .github/workflows/linter.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 63167ade312..af1130187d5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -8,7 +8,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: 1.x diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c612df5ee44..c87fde4c96b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ matrix.go-version }} - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Get values for cache paths to be used in later steps - id: cache-paths From 95c2a13b15665ac2e85de2441bbe919d73496777 Mon Sep 17 00:00:00 2001 From: tomfeigin Date: Tue, 9 Dec 2025 00:36:19 +0200 Subject: [PATCH 25/49] fix!: Change Org usage report `Quantity` to `float64` (#3862) BREAKING CHANGE: `UsageItem.Quantity` is now type `float64`. --- github/billing.go | 2 +- github/billing_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/github/billing.go b/github/billing.go index bb6f8ec49ea..8a0e55c02bf 100644 --- a/github/billing.go +++ b/github/billing.go @@ -121,7 +121,7 @@ type UsageItem struct { Date string `json:"date"` Product string `json:"product"` SKU string `json:"sku"` - Quantity int `json:"quantity"` + Quantity float64 `json:"quantity"` UnitType string `json:"unitType"` PricePerUnit float64 `json:"pricePerUnit"` GrossAmount float64 `json:"grossAmount"` diff --git a/github/billing_test.go b/github/billing_test.go index 7d1baa9719f..fdd7c471972 100644 --- a/github/billing_test.go +++ b/github/billing_test.go @@ -407,7 +407,7 @@ func TestBillingService_GetOrganizationUsageReport(t *testing.T) { Date: "2023-08-01", Product: "Actions", SKU: "Actions Linux", - Quantity: 100, + Quantity: 100.0, UnitType: "minutes", PricePerUnit: 0.008, GrossAmount: 0.8, @@ -488,7 +488,7 @@ func TestBillingService_GetUsageReport(t *testing.T) { Date: "2023-08-15", Product: "Codespaces", SKU: "Codespaces Linux", - Quantity: 50, + Quantity: 50.0, UnitType: "hours", PricePerUnit: 0.18, GrossAmount: 9.0, From 06ab3a27351116cb8d0d93f99fe5ab7ac7b5c6b2 Mon Sep 17 00:00:00 2001 From: Scott Dawson Date: Tue, 9 Dec 2025 12:25:04 +1100 Subject: [PATCH 26/49] feat: Add `advanced_search` parameter to `SearchOptions` (#3868) --- github/github-accessors.go | 8 ++++++++ github/github-accessors_test.go | 11 +++++++++++ github/search.go | 3 +++ github/search_test.go | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/github/github-accessors.go b/github/github-accessors.go index fa729b5f565..63fbc951881 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -26646,6 +26646,14 @@ func (s *SCIMUserRole) GetType() string { return *s.Type } +// GetAdvancedSearch returns the AdvancedSearch field if it's non-nil, zero value otherwise. +func (s *SearchOptions) GetAdvancedSearch() bool { + if s == nil || s.AdvancedSearch == nil { + return false + } + return *s.AdvancedSearch +} + // GetStatus returns the Status field if it's non-nil, zero value otherwise. func (s *SecretScanning) GetStatus() string { if s == nil || s.Status == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index ca457c5ce98..ddf09fa3122 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -34393,6 +34393,17 @@ func TestSCIMUserRole_GetType(tt *testing.T) { s.GetType() } +func TestSearchOptions_GetAdvancedSearch(tt *testing.T) { + tt.Parallel() + var zeroValue bool + s := &SearchOptions{AdvancedSearch: &zeroValue} + s.GetAdvancedSearch() + s = &SearchOptions{} + s.GetAdvancedSearch() + s = nil + s.GetAdvancedSearch() +} + func TestSecretScanning_GetStatus(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/search.go b/github/search.go index 9b9f937033d..aa99786f728 100644 --- a/github/search.go +++ b/github/search.go @@ -54,6 +54,9 @@ type SearchOptions struct { // Whether to retrieve text match metadata with a query TextMatch bool `url:"-"` + // Whether to enable advanced search for issues + AdvancedSearch *bool `url:"advanced_search,omitempty"` + ListOptions } diff --git a/github/search_test.go b/github/search_test.go index aec5b56c17e..91a2bab401e 100644 --- a/github/search_test.go +++ b/github/search_test.go @@ -265,6 +265,41 @@ func TestSearchService_Issues(t *testing.T) { } } +func TestSearchService_Issues_advancedSearch(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/search/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "q": "blah", + "sort": "forks", + "order": "desc", + "page": "2", + "per_page": "2", + "advanced_search": "true", + }) + + fmt.Fprint(w, `{"total_count": 4, "incomplete_results": true, "items": [{"number":1},{"number":2}]}`) + }) + + opts := &SearchOptions{Sort: "forks", Order: "desc", ListOptions: ListOptions{Page: 2, PerPage: 2}, AdvancedSearch: Ptr(true)} + ctx := t.Context() + result, _, err := client.Search.Issues(ctx, "blah", opts) + if err != nil { + t.Errorf("Search.Issues_advancedSearch returned error: %v", err) + } + + want := &IssuesSearchResult{ + Total: Ptr(4), + IncompleteResults: Ptr(true), + Issues: []*Issue{{Number: Ptr(1)}, {Number: Ptr(2)}}, + } + if !cmp.Equal(result, want) { + t.Errorf("Search.Issues_advancedSearch returned %+v, want %+v", result, want) + } +} + func TestSearchService_Issues_coverage(t *testing.T) { t.Parallel() client, _, _ := setup(t) From e716505f5891825678d973cac47780e637512172 Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:44:54 +0100 Subject: [PATCH 27/49] feat: Implement Enterprise SCIM - Delete Groups or Users (#3856) --- github/enterprise_scim.go | 61 ++++++++++++++++++++++++++++++++++ github/enterprise_scim_test.go | 54 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index 8407857c11d..1d020f27f86 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -162,6 +162,9 @@ type SCIMEnterpriseAttributeOperation struct { // ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise. // +// You can improve query search time by using the `excludedAttributes` query +// parameter with a value of `members` to exclude members from the response. +// // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise // //meta:operation GET /scim/v2/enterprises/{enterprise}/Groups @@ -189,6 +192,10 @@ func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enter // ListProvisionedSCIMUsers lists provisioned SCIM enterprise users. // +// When members are part of the group provisioning payload, they're designated +// as external group members. Providers are responsible for maintaining a +// mapping between the `externalId` and `id` for each user. +// // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise // //meta:operation GET /scim/v2/enterprises/{enterprise}/Users @@ -270,6 +277,14 @@ func (s *EnterpriseService) SetProvisionedSCIMUser(ctx context.Context, enterpri // UpdateSCIMGroupAttribute updates a provisioned group’s individual attributes. // +// The `attribute` parameter must include at least one of the following +// Operations: `add`, `remove`, or `replace`. +// +// The update function can also be used to add group memberships. +// +// You can submit group memberships individually or in batches for improved +// efficiency. +// // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-group // //meta:operation PATCH /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} @@ -292,6 +307,16 @@ func (s *EnterpriseService) UpdateSCIMGroupAttribute(ctx context.Context, enterp // UpdateSCIMUserAttribute updates a provisioned user's individual attributes. // +// The `attribute` parameter must include at least one of the following +// Operations: `add`, `remove`, or `replace`. +// +// Note: Complex SCIM path selectors that include filters are not supported. +// For example, a path selector defined as `"path": "emails[type eq \"work\"]"` +// will be ineffective. +// +// Warning: Setting `active: false` will suspend a user, and their handle and +// email will be obfuscated. +// // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#update-an-attribute-for-a-scim-enterprise-user // //meta:operation PATCH /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} @@ -311,3 +336,39 @@ func (s *EnterpriseService) UpdateSCIMUserAttribute(ctx context.Context, enterpr return user, resp, nil } + +// DeleteSCIMGroup deletes a SCIM group from an enterprise. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#delete-a-scim-group-from-an-enterprise +// +//meta:operation DELETE /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} +func (s *EnterpriseService) DeleteSCIMGroup(ctx context.Context, enterprise, scimGroupID string) (*Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Groups/%v", enterprise, scimGroupID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// DeleteSCIMUser deletes a SCIM user from an enterprise. +// +// Suspends a SCIM user permanently from an enterprise. This action will: +// remove all the user's data, anonymize their login, email, and display name, +// erase all external identity SCIM attributes, delete the user's emails, +// avatar, PATs, SSH keys, OAuth authorizations, GPG keys, and SAML mappings. +// This action is irreversible. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#delete-a-scim-user-from-an-enterprise +// +//meta:operation DELETE /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} +func (s *EnterpriseService) DeleteSCIMUser(ctx context.Context, enterprise, scimUserID string) (*Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users/%v", enterprise, scimUserID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index 51bb80eb64e..b004a670a1a 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -809,3 +809,57 @@ func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) { return resp, err }) } + +func TestEnterpriseService_DeleteSCIMGroup(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Groups/abcd", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testHeader(t, r, "Accept", mediaTypeV3) + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + _, err := client.Enterprise.DeleteSCIMGroup(ctx, "ee", "abcd") + if err != nil { + t.Fatalf("Enterprise.DeleteSCIMGroup returned unexpected error: %v", err) + } + + const methodName = "DeleteSCIMGroup" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Enterprise.DeleteSCIMGroup(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.DeleteSCIMGroup(ctx, "ee", "abcd") + }) +} + +func TestEnterpriseService_DeleteSCIMUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Users/7fce", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testHeader(t, r, "Accept", mediaTypeV3) + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + _, err := client.Enterprise.DeleteSCIMUser(ctx, "ee", "7fce") + if err != nil { + t.Fatalf("Enterprise.DeleteSCIMUser returned unexpected error: %v", err) + } + + const methodName = "DeleteSCIMUser" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Enterprise.DeleteSCIMUser(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.DeleteSCIMUser(ctx, "ee", "7fce") + }) +} From c7473ca3b6d6f4004682b4338df6546a945e4c84 Mon Sep 17 00:00:00 2001 From: JiayangZhou <47085823+JiayangZhou@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:58:36 -0800 Subject: [PATCH 28/49] feat: Add support for `copilot_code_review` rule type (#3857) --- github/github-accessors.go | 8 +++ github/github-accessors_test.go | 8 +++ github/rules.go | 38 ++++++++++++- github/rules_test.go | 97 ++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 4 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index 63fbc951881..ffd553c6381 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -25062,6 +25062,14 @@ func (r *RepositoryRulesetRules) GetCommitterEmailPattern() *PatternRuleParamete return r.CommitterEmailPattern } +// GetCopilotCodeReview returns the CopilotCodeReview field. +func (r *RepositoryRulesetRules) GetCopilotCodeReview() *CopilotCodeReviewRuleParameters { + if r == nil { + return nil + } + return r.CopilotCodeReview +} + // GetCreation returns the Creation field. func (r *RepositoryRulesetRules) GetCreation() *EmptyRuleParameters { if r == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index ddf09fa3122..46f914c99d6 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -32353,6 +32353,14 @@ func TestRepositoryRulesetRules_GetCommitterEmailPattern(tt *testing.T) { r.GetCommitterEmailPattern() } +func TestRepositoryRulesetRules_GetCopilotCodeReview(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetCopilotCodeReview() + r = nil + r.GetCopilotCodeReview() +} + func TestRepositoryRulesetRules_GetCreation(tt *testing.T) { tt.Parallel() r := &RepositoryRulesetRules{} diff --git a/github/rules.go b/github/rules.go index 2326ed0d7ac..76d6cbc0095 100644 --- a/github/rules.go +++ b/github/rules.go @@ -75,6 +75,7 @@ const ( RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" + RulesetRuleTypeCopilotCodeReview RepositoryRuleType = "copilot_code_review" RulesetRuleTypeCreation RepositoryRuleType = "creation" RulesetRuleTypeDeletion RepositoryRuleType = "deletion" RulesetRuleTypeMergeQueue RepositoryRuleType = "merge_queue" @@ -306,6 +307,7 @@ type RepositoryRulesetRules struct { TagNamePattern *PatternRuleParameters Workflows *WorkflowsRuleParameters CodeScanning *CodeScanningRuleParameters + CopilotCodeReview *CopilotCodeReviewRuleParameters // Push target rules. FileExtensionRestriction *FileExtensionRestrictionRuleParameters @@ -539,6 +541,12 @@ type CodeScanningRuleParameters struct { CodeScanningTools []*RuleCodeScanningTool `json:"code_scanning_tools"` } +// CopilotCodeReviewRuleParameters represents the copilot_code_review rule parameters. +type CopilotCodeReviewRuleParameters struct { + ReviewNewPushes bool `json:"review_new_pushes"` + ReviewDraftPullRequests bool `json:"review_draft_pull_requests"` +} + // RuleCodeScanningTool represents a single code scanning tool for the code scanning parameters. type RuleCodeScanningTool struct { AlertsThreshold CodeScanningAlertsThreshold `json:"alerts_threshold"` @@ -566,9 +574,9 @@ type repositoryRulesetRuleWrapper struct { // MarshalJSON is a custom JSON marshaler for RulesetRules. func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { - // The RepositoryRulesetRules type marshals to between 1 and 21 rules. + // The RepositoryRulesetRules type marshals to between 1 and 22 rules. // If new rules are added to RepositoryRulesetRules the capacity below needs increasing - rawRules := make([]json.RawMessage, 0, 21) + rawRules := make([]json.RawMessage, 0, 22) if r.Creation != nil { bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeCreation, r.Creation) @@ -738,6 +746,14 @@ func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { rawRules = append(rawRules, json.RawMessage(bytes)) } + if r.CopilotCodeReview != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeCopilotCodeReview, r.CopilotCodeReview) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + if r.RepositoryCreate != nil { bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryCreate, r.RepositoryCreate) if err != nil { @@ -965,6 +981,14 @@ func (r *RepositoryRulesetRules) UnmarshalJSON(data []byte) error { return err } } + case RulesetRuleTypeCopilotCodeReview: + r.CopilotCodeReview = &CopilotCodeReviewRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.CopilotCodeReview); err != nil { + return err + } + } case RulesetRuleTypeRepositoryCreate: r.RepositoryCreate = &EmptyRuleParameters{} case RulesetRuleTypeRepositoryDelete: @@ -1366,6 +1390,16 @@ func (r *RepositoryRule) UnmarshalJSON(data []byte) error { } } + r.Parameters = p + case RulesetRuleTypeCopilotCodeReview: + p := &CopilotCodeReviewRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } + r.Parameters = p case RulesetRuleTypeRepositoryCreate: r.Parameters = nil diff --git a/github/rules_test.go b/github/rules_test.go index cd0f2ae2377..e0b091ecbd0 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -122,13 +122,17 @@ func TestRulesetRules(t *testing.T) { }, }, }, + CopilotCodeReview: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, RepositoryCreate: &EmptyRuleParameters{}, RepositoryDelete: &EmptyRuleParameters{}, RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, RepositoryTransfer: &EmptyRuleParameters{}, RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, + `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, { "all_rules_with_all_params", @@ -240,13 +244,17 @@ func TestRulesetRules(t *testing.T) { }, }, }, + CopilotCodeReview: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, RepositoryCreate: &EmptyRuleParameters{}, RepositoryDelete: &EmptyRuleParameters{}, RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, RepositoryTransfer: &EmptyRuleParameters{}, RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, + `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, } @@ -298,6 +306,39 @@ func TestRulesetRules(t *testing.T) { }) } }) + + t.Run("UnmarshalJSON_Error", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + }{ + { + "invalid_copilot_code_review_bool", + `[{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}]`, + }, + { + "invalid_copilot_code_review_draft_pr", + `[{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}]`, + }, + { + "invalid_copilot_code_review_parameters", + `[{"type":"copilot_code_review","parameters":"not_an_object"}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := &RepositoryRulesetRules{} + err := json.Unmarshal([]byte(tt.json), got) + if err == nil { + t.Errorf("Expected error unmarshaling %q, got nil", tt.json) + } + }) + } + }) } func TestBranchRules(t *testing.T) { @@ -929,6 +970,25 @@ func TestRepositoryRule(t *testing.T) { }, `{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}`, }, + { + "copilot_code_review", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, + }, + `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}}`, + }, + { + "copilot_code_review_empty_params", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{}, + }, + `{"type":"copilot_code_review","parameters":{"review_new_pushes":false,"review_draft_pull_requests":false}}`, + }, { "repository_create", &RepositoryRule{Type: RulesetRuleTypeRepositoryCreate, Parameters: nil}, @@ -992,4 +1052,37 @@ func TestRepositoryRule(t *testing.T) { }) } }) + + t.Run("UnmarshalJSON_Error", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + }{ + { + "invalid_copilot_code_review_bool", + `{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}`, + }, + { + "invalid_copilot_code_review_draft_pr", + `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}`, + }, + { + "invalid_copilot_code_review_parameters", + `{"type":"copilot_code_review","parameters":"not_an_object"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := &RepositoryRule{} + err := json.Unmarshal([]byte(tt.json), got) + if err == nil { + t.Errorf("Expected error unmarshaling %q, got nil", tt.json) + } + }) + } + }) } From f093aaa2d33d06609e8b38440f45b0cd75f61087 Mon Sep 17 00:00:00 2001 From: Aaron LaBrie Date: Tue, 9 Dec 2025 15:21:47 -0600 Subject: [PATCH 29/49] chore!: Remove `PullRequestRuleParameters.AutomaticCopilotCodeReviewEnabled` field (#3866) BREAKING CHANGE: `PullRequestRuleParameters.AutomaticCopilotCodeReviewEnabled` is now removed. --- github/event_types_test.go | 39 +++++++++++++++------------------ github/github-accessors.go | 8 ------- github/github-accessors_test.go | 11 ---------- github/rules.go | 15 ++++++------- github/rules_test.go | 37 ++++++++++++++----------------- 5 files changed, 42 insertions(+), 68 deletions(-) diff --git a/github/event_types_test.go b/github/event_types_test.go index 9ad223dca69..15881ab6cec 100644 --- a/github/event_types_test.go +++ b/github/event_types_test.go @@ -9789,7 +9789,7 @@ func TestRepositoryRulesetEvent_Unmarshal(t *testing.T) { { "created", fmt.Sprintf( - `{"action":"created","repository_ruleset":{"id":1,"name":"r","target":"branch","source_type":"Repository","source":"o/r","enforcement":"active","conditions":{"ref_name":{"exclude":[],"include":["~ALL"]}},"rules":[{"type":"deletion"},{"type":"creation"},{"type":"update"},{"type":"required_linear_history"},{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"automatic_copilot_code_review_enabled":false,"allowed_merge_methods":["squash","rebase","merge"]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"high_or_higher","alerts_threshold":"errors"}]}}],"node_id":"n","created_at":%[1]s,"updated_at":%[1]s,"_links":{"self":{"href":"a"},"html":{"href":"a"}}},"repository":{"id":1,"node_id":"n","name":"r","full_name":"o/r"},"organization":{"id":1,"node_id":"n","name":"o"},"enterprise":{"id":1,"node_id":"n","slug":"e","name":"e"},"installation":{"id":1,"node_id":"n","app_id":1,"app_slug":"a"},"sender":{"id":1,"node_id":"n","login":"l"}}`, + `{"action":"created","repository_ruleset":{"id":1,"name":"r","target":"branch","source_type":"Repository","source":"o/r","enforcement":"active","conditions":{"ref_name":{"exclude":[],"include":["~ALL"]}},"rules":[{"type":"deletion"},{"type":"creation"},{"type":"update"},{"type":"required_linear_history"},{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"allowed_merge_methods":["squash","rebase","merge"]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"high_or_higher","alerts_threshold":"errors"}]}}],"node_id":"n","created_at":%[1]s,"updated_at":%[1]s,"_links":{"self":{"href":"a"},"html":{"href":"a"}}},"repository":{"id":1,"node_id":"n","name":"r","full_name":"o/r"},"organization":{"id":1,"node_id":"n","name":"o"},"enterprise":{"id":1,"node_id":"n","slug":"e","name":"e"},"installation":{"id":1,"node_id":"n","app_id":1,"app_slug":"a"},"sender":{"id":1,"node_id":"n","login":"l"}}`, referenceTimeStr, ), &RepositoryRulesetEvent{ @@ -9818,12 +9818,11 @@ func TestRepositoryRulesetEvent_Unmarshal(t *testing.T) { PullRequestMergeMethodRebase, PullRequestMergeMethodMerge, }, - AutomaticCopilotCodeReviewEnabled: Ptr(false), - DismissStaleReviewsOnPush: false, - RequireCodeOwnerReview: false, - RequireLastPushApproval: false, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: false, + DismissStaleReviewsOnPush: false, + RequireCodeOwnerReview: false, + RequireLastPushApproval: false, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: false, }, CodeScanning: &CodeScanningRuleParameters{ CodeScanningTools: []*RuleCodeScanningTool{ @@ -9853,7 +9852,7 @@ func TestRepositoryRulesetEvent_Unmarshal(t *testing.T) { { "edited", fmt.Sprintf( - `{"action":"edited","repository_ruleset":{"id":1,"name":"r","target":"branch","source_type":"Repository","source":"o/r","enforcement":"active","conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH","refs/heads/dev-*"]}},"rules":[{"type":"deletion"},{"type":"creation"},{"type":"update"},{"type": "required_signatures"},{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"automatic_copilot_code_review_enabled":false,"allowed_merge_methods":["squash","rebase"]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"medium_or_higher","alerts_threshold":"errors"}]}}],"node_id":"n","created_at":%[1]s,"updated_at":%[1]s,"_links":{"self":{"href":"a"},"html":{"href":"a"}}},"changes":{"rules":{"added":[{"type": "required_signatures"}],"updated":[{"rule":{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"automatic_copilot_code_review_enabled":false,"allowed_merge_methods":["squash","rebase"]}},"changes":{"configuration":{"from":"{\\\"required_reviewers\\\":[],\\\"allowed_merge_methods\\\":[\\\"squash\\\",\\\"rebase\\\",\\\"merge\\\"],\\\"require_code_owner_review\\\":false,\\\"require_last_push_approval\\\":false,\\\"dismiss_stale_reviews_on_push\\\":false,\\\"required_approving_review_count\\\":2,\\\"authorized_dismissal_actors_only\\\":false,\\\"required_review_thread_resolution\\\":false,\\\"ignore_approvals_from_contributors\\\":false,\\\"automatic_copilot_code_review_enabled\\\":false}"}}},{"rule":{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"medium_or_higher","alerts_threshold":"errors"}]}},"changes":{"configuration":{"from":"{\\\"code_scanning_tools\\\":[{\\\"tool\\\":\\\"CodeQL\\\",\\\"alerts_threshold\\\":\\\"errors\\\",\\\"security_alerts_threshold\\\":\\\"high_or_higher\\\"}]}"}}}],"deleted":[{"type":"required_linear_history"}]},"conditions":{"updated":[{"condition":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH","refs/heads/dev-*"]}},"changes":{"include":{"from":["~ALL"]}}}],"deleted":[]}},"repository":{"id":1,"node_id":"n","name":"r","full_name":"o/r"},"organization":{"id":1,"node_id":"n","name":"o"},"enterprise":{"id":1,"node_id":"n","slug":"e","name":"e"},"installation":{"id":1,"node_id":"n","app_id":1,"app_slug":"a"},"sender":{"id":1,"node_id":"n","login":"l"}}`, + `{"action":"edited","repository_ruleset":{"id":1,"name":"r","target":"branch","source_type":"Repository","source":"o/r","enforcement":"active","conditions":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH","refs/heads/dev-*"]}},"rules":[{"type":"deletion"},{"type":"creation"},{"type":"update"},{"type": "required_signatures"},{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"allowed_merge_methods":["squash","rebase"]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"medium_or_higher","alerts_threshold":"errors"}]}}],"node_id":"n","created_at":%[1]s,"updated_at":%[1]s,"_links":{"self":{"href":"a"},"html":{"href":"a"}}},"changes":{"rules":{"added":[{"type": "required_signatures"}],"updated":[{"rule":{"type":"pull_request","parameters":{"required_approving_review_count":2,"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_review_thread_resolution":false,"allowed_merge_methods":["squash","rebase"]}},"changes":{"configuration":{"from":"{\\\"required_reviewers\\\":[],\\\"allowed_merge_methods\\\":[\\\"squash\\\",\\\"rebase\\\",\\\"merge\\\"],\\\"require_code_owner_review\\\":false,\\\"require_last_push_approval\\\":false,\\\"dismiss_stale_reviews_on_push\\\":false,\\\"required_approving_review_count\\\":2,\\\"authorized_dismissal_actors_only\\\":false,\\\"required_review_thread_resolution\\\":false,\\\"ignore_approvals_from_contributors\\\":false}"}}},{"rule":{"type":"code_scanning","parameters":{"code_scanning_tools":[{"tool":"CodeQL","security_alerts_threshold":"medium_or_higher","alerts_threshold":"errors"}]}},"changes":{"configuration":{"from":"{\\\"code_scanning_tools\\\":[{\\\"tool\\\":\\\"CodeQL\\\",\\\"alerts_threshold\\\":\\\"errors\\\",\\\"security_alerts_threshold\\\":\\\"high_or_higher\\\"}]}"}}}],"deleted":[{"type":"required_linear_history"}]},"conditions":{"updated":[{"condition":{"ref_name":{"exclude":[],"include":["~DEFAULT_BRANCH","refs/heads/dev-*"]}},"changes":{"include":{"from":["~ALL"]}}}],"deleted":[]}},"repository":{"id":1,"node_id":"n","name":"r","full_name":"o/r"},"organization":{"id":1,"node_id":"n","name":"o"},"enterprise":{"id":1,"node_id":"n","slug":"e","name":"e"},"installation":{"id":1,"node_id":"n","app_id":1,"app_slug":"a"},"sender":{"id":1,"node_id":"n","login":"l"}}`, referenceTimeStr, ), &RepositoryRulesetEvent{ @@ -9881,12 +9880,11 @@ func TestRepositoryRulesetEvent_Unmarshal(t *testing.T) { PullRequestMergeMethodSquash, PullRequestMergeMethodRebase, }, - AutomaticCopilotCodeReviewEnabled: Ptr(false), - DismissStaleReviewsOnPush: false, - RequireCodeOwnerReview: false, - RequireLastPushApproval: false, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: false, + DismissStaleReviewsOnPush: false, + RequireCodeOwnerReview: false, + RequireLastPushApproval: false, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: false, }, CodeScanning: &CodeScanningRuleParameters{ CodeScanningTools: []*RuleCodeScanningTool{ @@ -9936,18 +9934,17 @@ func TestRepositoryRulesetEvent_Unmarshal(t *testing.T) { PullRequestMergeMethodSquash, PullRequestMergeMethodRebase, }, - AutomaticCopilotCodeReviewEnabled: Ptr(false), - DismissStaleReviewsOnPush: false, - RequireCodeOwnerReview: false, - RequireLastPushApproval: false, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: false, + DismissStaleReviewsOnPush: false, + RequireCodeOwnerReview: false, + RequireLastPushApproval: false, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: false, }, }, Changes: &RepositoryRulesetChangedRule{ Configuration: &RepositoryRulesetChangeSource{ From: Ptr( - `{\"required_reviewers\":[],\"allowed_merge_methods\":[\"squash\",\"rebase\",\"merge\"],\"require_code_owner_review\":false,\"require_last_push_approval\":false,\"dismiss_stale_reviews_on_push\":false,\"required_approving_review_count\":2,\"authorized_dismissal_actors_only\":false,\"required_review_thread_resolution\":false,\"ignore_approvals_from_contributors\":false,\"automatic_copilot_code_review_enabled\":false}`, + `{\"required_reviewers\":[],\"allowed_merge_methods\":[\"squash\",\"rebase\",\"merge\"],\"require_code_owner_review\":false,\"require_last_push_approval\":false,\"dismiss_stale_reviews_on_push\":false,\"required_approving_review_count\":2,\"authorized_dismissal_actors_only\":false,\"required_review_thread_resolution\":false,\"ignore_approvals_from_contributors\":false}`, ), }, }, diff --git a/github/github-accessors.go b/github/github-accessors.go index ffd553c6381..9822bf440af 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -21406,14 +21406,6 @@ func (p *PullRequestReviewThreadEvent) GetThread() *PullRequestThread { return p.Thread } -// GetAutomaticCopilotCodeReviewEnabled returns the AutomaticCopilotCodeReviewEnabled field if it's non-nil, zero value otherwise. -func (p *PullRequestRuleParameters) GetAutomaticCopilotCodeReviewEnabled() bool { - if p == nil || p.AutomaticCopilotCodeReviewEnabled == nil { - return false - } - return *p.AutomaticCopilotCodeReviewEnabled -} - // GetAction returns the Action field if it's non-nil, zero value otherwise. func (p *PullRequestTargetEvent) GetAction() string { if p == nil || p.Action == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 46f914c99d6..f9b61486858 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -27674,17 +27674,6 @@ func TestPullRequestReviewThreadEvent_GetThread(tt *testing.T) { p.GetThread() } -func TestPullRequestRuleParameters_GetAutomaticCopilotCodeReviewEnabled(tt *testing.T) { - tt.Parallel() - var zeroValue bool - p := &PullRequestRuleParameters{AutomaticCopilotCodeReviewEnabled: &zeroValue} - p.GetAutomaticCopilotCodeReviewEnabled() - p = &PullRequestRuleParameters{} - p.GetAutomaticCopilotCodeReviewEnabled() - p = nil - p.GetAutomaticCopilotCodeReviewEnabled() -} - func TestPullRequestTargetEvent_GetAction(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/rules.go b/github/rules.go index 76d6cbc0095..1efb1783f92 100644 --- a/github/rules.go +++ b/github/rules.go @@ -457,14 +457,13 @@ type RequiredDeploymentsRuleParameters struct { // PullRequestRuleParameters represents the pull_request rule parameters. type PullRequestRuleParameters struct { - AllowedMergeMethods []PullRequestMergeMethod `json:"allowed_merge_methods"` - AutomaticCopilotCodeReviewEnabled *bool `json:"automatic_copilot_code_review_enabled,omitempty"` - DismissStaleReviewsOnPush bool `json:"dismiss_stale_reviews_on_push"` - RequireCodeOwnerReview bool `json:"require_code_owner_review"` - RequireLastPushApproval bool `json:"require_last_push_approval"` - RequiredApprovingReviewCount int `json:"required_approving_review_count"` - RequiredReviewers []*RulesetRequiredReviewer `json:"required_reviewers,omitempty"` - RequiredReviewThreadResolution bool `json:"required_review_thread_resolution"` + AllowedMergeMethods []PullRequestMergeMethod `json:"allowed_merge_methods"` + DismissStaleReviewsOnPush bool `json:"dismiss_stale_reviews_on_push"` + RequireCodeOwnerReview bool `json:"require_code_owner_review"` + RequireLastPushApproval bool `json:"require_last_push_approval"` + RequiredApprovingReviewCount int `json:"required_approving_review_count"` + RequiredReviewers []*RulesetRequiredReviewer `json:"required_reviewers,omitempty"` + RequiredReviewThreadResolution bool `json:"required_review_thread_resolution"` } // RulesetRequiredReviewer represents required reviewer parameters for pull requests in rulesets. diff --git a/github/rules_test.go b/github/rules_test.go index e0b091ecbd0..6e5e1691707 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -59,12 +59,11 @@ func TestRulesetRules(t *testing.T) { PullRequestMergeMethodSquash, PullRequestMergeMethodRebase, }, - AutomaticCopilotCodeReviewEnabled: nil, - DismissStaleReviewsOnPush: true, - RequireCodeOwnerReview: true, - RequireLastPushApproval: true, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: true, + DismissStaleReviewsOnPush: true, + RequireCodeOwnerReview: true, + RequireLastPushApproval: true, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: true, }, RequiredStatusChecks: &RequiredStatusChecksRuleParameters{ RequiredStatusChecks: []*RuleStatusCheck{ @@ -159,12 +158,11 @@ func TestRulesetRules(t *testing.T) { PullRequestMergeMethodSquash, PullRequestMergeMethodRebase, }, - AutomaticCopilotCodeReviewEnabled: Ptr(false), - DismissStaleReviewsOnPush: true, - RequireCodeOwnerReview: true, - RequireLastPushApproval: true, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: true, + DismissStaleReviewsOnPush: true, + RequireCodeOwnerReview: true, + RequireLastPushApproval: true, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: true, }, RequiredStatusChecks: &RequiredStatusChecksRuleParameters{ DoNotEnforceOnCreate: Ptr(true), @@ -254,7 +252,7 @@ func TestRulesetRules(t *testing.T) { RepositoryTransfer: &EmptyRuleParameters{}, RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, + `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, } @@ -777,15 +775,14 @@ func TestRepositoryRule(t *testing.T) { PullRequestMergeMethodSquash, PullRequestMergeMethodRebase, }, - AutomaticCopilotCodeReviewEnabled: Ptr(true), - DismissStaleReviewsOnPush: true, - RequireCodeOwnerReview: true, - RequireLastPushApproval: true, - RequiredApprovingReviewCount: 2, - RequiredReviewThreadResolution: true, + DismissStaleReviewsOnPush: true, + RequireCodeOwnerReview: true, + RequireLastPushApproval: true, + RequiredApprovingReviewCount: 2, + RequiredReviewThreadResolution: true, }, }, - `{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled": true,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}}`, + `{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}}`, }, { "pull_request_with_required_reviewers", From 4d9bd082d1f7e1281fdd122d08188c0a328cacc8 Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:51:30 +0100 Subject: [PATCH 30/49] feat!: Implement Enterprise SCIM - Provision Groups & Users (#3852) BREAKING CHANGE: `SCIMEnterpriseDisplayReference.Ref` is now of type `*string`. --- github/enterprise_scim.go | 54 +++++++- github/enterprise_scim_test.go | 222 ++++++++++++++++++++++++++++++-- github/github-accessors.go | 8 ++ github/github-accessors_test.go | 11 ++ 4 files changed, 281 insertions(+), 14 deletions(-) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index 1d020f27f86..bf8e6429365 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -32,17 +32,17 @@ const SCIMSchemasURINamespacesPatchOp = "urn:ietf:params:scim:api:messages:2.0:P type SCIMEnterpriseGroupAttributes struct { DisplayName *string `json:"displayName,omitempty"` // Human-readable name for a group. Members []*SCIMEnterpriseDisplayReference `json:"members,omitempty"` // List of members who are assigned to the group in SCIM provider - ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per user. + ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per group. + Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas. // Bellow: Only populated as a result of calling UpdateSCIMGroupAttribute: - Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas. - ID *string `json:"id,omitempty"` // The internally generated id for the group object. - Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group. + ID *string `json:"id,omitempty"` // The internally generated id for the group object. + Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group. } // SCIMEnterpriseDisplayReference represents a JSON SCIM (System for Cross-domain Identity Management) resource reference. type SCIMEnterpriseDisplayReference struct { Value string `json:"value"` // The local unique identifier for the member (e.g., user ID or group ID). - Ref string `json:"$ref"` // The URI reference to the member resource (e.g., https://api.github.com/scim/v2/Users/{id}). + Ref *string `json:"$ref,omitempty"` // The URI reference to the Members or Groups resource (e.g., /scim/v2/enterprises/{enterprise}/Users/{scim_user_id}). Display *string `json:"display,omitempty"` // The display name associated with the member (e.g., user name or group name). } @@ -337,6 +337,50 @@ func (s *EnterpriseService) UpdateSCIMUserAttribute(ctx context.Context, enterpr return user, resp, nil } +// ProvisionSCIMGroup creates a SCIM group for an enterprise. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-group +// +//meta:operation POST /scim/v2/enterprises/{enterprise}/Groups +func (s *EnterpriseService) ProvisionSCIMGroup(ctx context.Context, enterprise string, group SCIMEnterpriseGroupAttributes) (*SCIMEnterpriseGroupAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Groups", enterprise) + req, err := s.client.NewRequest("POST", u, group) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + groupProvisioned := new(SCIMEnterpriseGroupAttributes) + resp, err := s.client.Do(ctx, req, groupProvisioned) + if err != nil { + return nil, resp, err + } + + return groupProvisioned, resp, nil +} + +// ProvisionSCIMUser creates an external identity for a new SCIM enterprise user. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user +// +//meta:operation POST /scim/v2/enterprises/{enterprise}/Users +func (s *EnterpriseService) ProvisionSCIMUser(ctx context.Context, enterprise string, user SCIMEnterpriseUserAttributes) (*SCIMEnterpriseUserAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users", enterprise) + req, err := s.client.NewRequest("POST", u, user) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + userProvisioned := new(SCIMEnterpriseUserAttributes) + resp, err := s.client.Do(ctx, req, userProvisioned) + if err != nil { + return nil, resp, err + } + + return userProvisioned, resp, nil +} + // DeleteSCIMGroup deletes a SCIM group from an enterprise. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#delete-a-scim-group-from-an-enterprise diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index b004a670a1a..a0f984f818f 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -26,7 +26,7 @@ func TestSCIMEnterpriseGroups_Marshal(t *testing.T) { DisplayName: Ptr("gn1"), Members: []*SCIMEnterpriseDisplayReference{{ Value: "idm1", - Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/idm1", + Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/idm1"), Display: Ptr("m1"), }}, Schemas: []string{SCIMSchemasURINamespacesGroups}, @@ -94,7 +94,7 @@ func TestSCIMEnterpriseUsers_Marshal(t *testing.T) { UserName: "un1", Groups: []*SCIMEnterpriseDisplayReference{{ Value: "idgn1", - Ref: "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1", + Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1"), Display: Ptr("gn1"), }}, ID: Ptr("idun1"), @@ -209,7 +209,7 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) { DisplayName: Ptr("dn"), Members: []*SCIMEnterpriseDisplayReference{{ Value: "v", - Ref: "r", + Ref: Ptr("r"), Display: Ptr("d"), }}, ExternalID: Ptr("eid"), @@ -346,14 +346,14 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) { ExternalID: Ptr("de88"), Members: []*SCIMEnterpriseDisplayReference{{ Value: "e7f9", - Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9", + Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/e7f9"), Display: Ptr("d1"), }}, }}, } if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff) + t.Fatalf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff) } const methodName = "ListProvisionedSCIMGroups" @@ -461,7 +461,7 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) { } if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff) + t.Fatalf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff) } const methodName = "ListProvisionedSCIMUsers" @@ -662,7 +662,7 @@ func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) { DisplayName: Ptr("Employees"), Members: []*SCIMEnterpriseDisplayReference{{ Value: "879d", - Ref: "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d", + Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/879d"), Display: Ptr("User 1"), }}, Meta: &SCIMEnterpriseMeta{ @@ -687,7 +687,7 @@ func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) { t.Fatalf("Enterprise.UpdateSCIMGroupAttribute returned unexpected error: %v", err) } if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("Enterprise.UpdateSCIMGroupAttribute diff mismatch (-want +got):\n%v", diff) + t.Fatalf("Enterprise.UpdateSCIMGroupAttribute diff mismatch (-want +got):\n%v", diff) } const methodName = "UpdateSCIMGroupAttribute" @@ -792,7 +792,7 @@ func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) { t.Fatalf("Enterprise.UpdateSCIMUserAttribute returned unexpected error: %v", err) } if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("Enterprise.UpdateSCIMUserAttribute diff mismatch (-want +got):\n%v", diff) + t.Fatalf("Enterprise.UpdateSCIMUserAttribute diff mismatch (-want +got):\n%v", diff) } const methodName = "UpdateSCIMUserAttribute" @@ -810,6 +810,210 @@ func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) { }) } +func TestEnterpriseService_ProvisionSCIMGroup(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Groups", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"displayName":"dn","members":[{"value":"879d","display":"d1"},{"value":"0db5","display":"d2"}],"externalId":"8aa1","schemas":["`+SCIMSchemasURINamespacesGroups+`"]}`+"\n") + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesGroups+`"], + "id": "abcd", + "externalId": "8aa1", + "displayName": "dn", + "members": [ + { + "value": "879d", + "$ref": "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d", + "display": "d1" + }, + { + "value": "0db5", + "$ref": "https://api.github.localhost/scim/v2/enterprises/ee/Users/0db5", + "display": "d2" + } + ], + "meta": { + "resourceType": "Group", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd" + } + }`) + }) + want := &SCIMEnterpriseGroupAttributes{ + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ID: Ptr("abcd"), + ExternalID: Ptr("8aa1"), + DisplayName: Ptr("dn"), + Members: []*SCIMEnterpriseDisplayReference{{ + Value: "879d", + Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/879d"), + Display: Ptr("d1"), + }, { + Value: "0db5", + Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/0db5"), + Display: Ptr("d2"), + }}, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "Group", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseGroupAttributes{ + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ExternalID: Ptr("8aa1"), + DisplayName: Ptr("dn"), + Members: []*SCIMEnterpriseDisplayReference{{ + Value: "879d", + Display: Ptr("d1"), + }, { + Value: "0db5", + Display: Ptr("d2"), + }}, + } + got, _, err := client.Enterprise.ProvisionSCIMGroup(ctx, "ee", input) + if err != nil { + t.Fatalf("Enterprise.ProvisionSCIMGroup returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.ProvisionSCIMGroup diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "ProvisionSCIMGroup" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ProvisionSCIMGroup(ctx, "\n", SCIMEnterpriseGroupAttributes{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ProvisionSCIMGroup(ctx, "ee", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_ProvisionSCIMUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeSCIM) + testBody(t, r, `{"displayName":"DOE John","name":{"givenName":"John","familyName":"Doe","formatted":"John Doe"},"userName":"e123","emails":[{"value":"john@email.com","primary":true,"type":"work"}],"roles":[{"value":"User","primary":false}],"externalId":"e123","active":true,"schemas":["`+SCIMSchemasURINamespacesUser+`"]}`+"\n") + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesUser+`"], + "id": "7fce", + "externalId": "e123", + "active": true, + "userName": "e123", + "name": { + "formatted": "John Doe", + "familyName": "Doe", + "givenName": "John" + }, + "displayName": "DOE John", + "emails": [{ + "value": "john@email.com", + "type": "work", + "primary": true + }], + "roles": [{ + "value": "User", + "primary": false + }], + "meta": { + "resourceType": "User", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce" + } + }`) + }) + want := &SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ID: Ptr("7fce"), + ExternalID: "e123", + Active: true, + UserName: "e123", + DisplayName: "DOE John", + Name: &SCIMEnterpriseUserName{ + Formatted: Ptr("John Doe"), + FamilyName: "Doe", + GivenName: "John", + }, + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "john@email.com", + Type: "work", + Primary: true, + }}, + Roles: []*SCIMEnterpriseUserRole{{ + Value: "User", + Primary: Ptr(false), + }}, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce"), + }, + } + + ctx := t.Context() + input := SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ExternalID: "e123", + Active: true, + UserName: "e123", + Name: &SCIMEnterpriseUserName{ + Formatted: Ptr("John Doe"), + FamilyName: "Doe", + GivenName: "John", + }, + DisplayName: "DOE John", + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "john@email.com", + Type: "work", + Primary: true, + }}, + Roles: []*SCIMEnterpriseUserRole{{ + Value: "User", + Primary: Ptr(false), + }}, + } + got, _, err := client.Enterprise.ProvisionSCIMUser(ctx, "ee", input) + if err != nil { + t.Fatalf("Enterprise.ProvisionSCIMUser returned unexpected error: %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.ProvisionSCIMUser diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "ProvisionSCIMUser" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ProvisionSCIMUser(ctx, "\n", SCIMEnterpriseUserAttributes{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ProvisionSCIMUser(ctx, "ee", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestEnterpriseService_DeleteSCIMGroup(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/github-accessors.go b/github/github-accessors.go index 9822bf440af..6b7ec07c7e9 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -26334,6 +26334,14 @@ func (s *SCIMEnterpriseDisplayReference) GetDisplay() string { return *s.Display } +// GetRef returns the Ref field if it's non-nil, zero value otherwise. +func (s *SCIMEnterpriseDisplayReference) GetRef() string { + if s == nil || s.Ref == nil { + return "" + } + return *s.Ref +} + // GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise. func (s *SCIMEnterpriseGroupAttributes) GetDisplayName() string { if s == nil || s.DisplayName == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index f9b61486858..766bc5f2593 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -33973,6 +33973,17 @@ func TestSCIMEnterpriseDisplayReference_GetDisplay(tt *testing.T) { s.GetDisplay() } +func TestSCIMEnterpriseDisplayReference_GetRef(tt *testing.T) { + tt.Parallel() + var zeroValue string + s := &SCIMEnterpriseDisplayReference{Ref: &zeroValue} + s.GetRef() + s = &SCIMEnterpriseDisplayReference{} + s.GetRef() + s = nil + s.GetRef() +} + func TestSCIMEnterpriseGroupAttributes_GetDisplayName(tt *testing.T) { tt.Parallel() var zeroValue string From 1830689bcbcb8b16aa7a07f34f4f8a9712fc1478 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Wed, 10 Dec 2025 19:28:02 +0530 Subject: [PATCH 31/49] feat: Add support for Enterprise Team Members APIs (#3873) --- github/enterprise_team.go | 133 ++++++++++++++++++ github/enterprise_team_test.go | 245 +++++++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+) diff --git a/github/enterprise_team.go b/github/enterprise_team.go index 4cbcb89ed1d..716a330e281 100644 --- a/github/enterprise_team.go +++ b/github/enterprise_team.go @@ -150,3 +150,136 @@ func (s *EnterpriseService) DeleteTeam(ctx context.Context, enterprise, teamSlug return resp, nil } + +// ListTeamMembers lists all members of an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#list-members-in-an-enterprise-team +// +//meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships +func (s *EnterpriseService) ListTeamMembers(ctx context.Context, enterprise, enterpriseTeam string, opt *ListOptions) ([]*User, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships", enterprise, enterpriseTeam) + u, err := addOptions(u, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var members []*User + resp, err := s.client.Do(ctx, req, &members) + if err != nil { + return nil, resp, err + } + + return members, resp, nil +} + +// BulkAddTeamMembers adds multiple members to an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#bulk-add-team-members +// +//meta:operation POST /enterprises/{enterprise}/teams/{enterprise-team}/memberships/add +func (s *EnterpriseService) BulkAddTeamMembers(ctx context.Context, enterprise, enterpriseTeam string, username []string) ([]*User, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships/add", enterprise, enterpriseTeam) + req, err := s.client.NewRequest("POST", u, map[string][]string{"usernames": username}) + if err != nil { + return nil, nil, err + } + + var members []*User + resp, err := s.client.Do(ctx, req, &members) + if err != nil { + return nil, resp, err + } + + return members, resp, nil +} + +// BulkRemoveTeamMembers removes multiple members from an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#bulk-remove-team-members +// +//meta:operation POST /enterprises/{enterprise}/teams/{enterprise-team}/memberships/remove +func (s *EnterpriseService) BulkRemoveTeamMembers(ctx context.Context, enterprise, enterpriseTeam string, username []string) ([]*User, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships/remove", enterprise, enterpriseTeam) + req, err := s.client.NewRequest("POST", u, map[string][]string{"usernames": username}) + if err != nil { + return nil, nil, err + } + + var members []*User + resp, err := s.client.Do(ctx, req, &members) + if err != nil { + return nil, resp, err + } + + return members, resp, nil +} + +// GetTeamMembership retrieves a team membership for a user in an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#get-enterprise-team-membership +// +//meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} +func (s *EnterpriseService) GetTeamMembership(ctx context.Context, enterprise, enterpriseTeam, username string) (*User, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships/%v", enterprise, enterpriseTeam, username) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var membership *User + resp, err := s.client.Do(ctx, req, &membership) + if err != nil { + return nil, resp, err + } + + return membership, resp, nil +} + +// AddTeamMember adds a member to an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#add-team-member +// +//meta:operation PUT /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} +func (s *EnterpriseService) AddTeamMember(ctx context.Context, enterprise, enterpriseTeam, username string) (*User, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships/%v", enterprise, enterpriseTeam, username) + + req, err := s.client.NewRequest("PUT", u, nil) + if err != nil { + return nil, nil, err + } + + var member *User + resp, err := s.client.Do(ctx, req, &member) + if err != nil { + return nil, resp, err + } + + return member, resp, nil +} + +// RemoveTeamMember removes a member from an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#remove-team-membership +// +//meta:operation DELETE /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} +func (s *EnterpriseService) RemoveTeamMember(ctx context.Context, enterprise, enterpriseTeam, username string) (*Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/memberships/%v", enterprise, enterpriseTeam, username) + + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/enterprise_team_test.go b/github/enterprise_team_test.go index 573b1ec36d1..a31872653d3 100644 --- a/github/enterprise_team_test.go +++ b/github/enterprise_team_test.go @@ -234,3 +234,248 @@ func TestEnterpriseService_DeleteTeam(t *testing.T) { return client.Enterprise.DeleteTeam(ctx, "e", "t1") }) } + +func TestEnterpriseService_ListTeamMembers(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{ + "login": "user1", + "id": 1001, + "url": "https://example.com/user1" + }]`) + }) + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + got, _, err := client.Enterprise.ListTeamMembers(ctx, "e", "t1", opts) + if err != nil { + t.Fatalf("Enterprise.ListTeamMembers returned error: %v", err) + } + + want := []*User{ + { + Login: Ptr("user1"), + ID: Ptr(int64(1001)), + URL: Ptr("https://example.com/user1"), + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.ListTeamMembers = %+v, want %+v", got, want) + } + + const methodName = "ListTeamMembers" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.ListTeamMembers(ctx, "\n", "t1", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListTeamMembers(ctx, "e", "t1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_BulkAddTeamMembers(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships/add", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + fmt.Fprint(w, `[{ + "login": "u1", + "id": 1 + },{ + "login": "u2", + "id": 2 + }]`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.BulkAddTeamMembers(ctx, "e", "t1", []string{"u1", "u2"}) + if err != nil { + t.Fatalf("BulkAddTeamMembers returned error: %v", err) + } + + want := []*User{ + {Login: Ptr("u1"), ID: Ptr(int64(1))}, + {Login: Ptr("u2"), ID: Ptr(int64(2))}, + } + + if !cmp.Equal(got, want) { + t.Errorf("BulkAddTeamMembers = %+v, want %+v", got, want) + } + + const methodName = "BulkAddTeamMembers" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.BulkAddTeamMembers(ctx, "\n", "t1", []string{"u1"}) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.BulkAddTeamMembers(ctx, "e", "t1", []string{"u1"}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_BulkRemoveTeamMembers(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships/remove", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + fmt.Fprint(w, `[{ + "login": "u1", + "id": 1 + },{ + "login": "u2", + "id": 2 + }]`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.BulkRemoveTeamMembers(ctx, "e", "t1", []string{"u1", "u2"}) + if err != nil { + t.Fatalf("BulkRemoveTeamMembers returned error: %v", err) + } + + want := []*User{ + {Login: Ptr("u1"), ID: Ptr(int64(1))}, + {Login: Ptr("u2"), ID: Ptr(int64(2))}, + } + + if !cmp.Equal(got, want) { + t.Errorf("BulkRemoveTeamMembers = %+v, want %+v", got, want) + } + + const methodName = "BulkRemoveTeamMembers" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.BulkRemoveTeamMembers(ctx, "\n", "t1", []string{"u1"}) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.BulkRemoveTeamMembers(ctx, "e", "t1", []string{"u1"}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_GetTeamMembership(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships/u1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "login": "u1", + "id": 10 + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.GetTeamMembership(ctx, "e", "t1", "u1") + if err != nil { + t.Fatalf("GetTeamMembership returned error: %v", err) + } + + want := &User{ + Login: Ptr("u1"), + ID: Ptr(int64(10)), + } + + if !cmp.Equal(got, want) { + t.Errorf("GetTeamMembership = %+v, want %+v", got, want) + } + + const methodName = "GetTeamMembership" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.GetTeamMembership(ctx, "\n", "t1", "u1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetTeamMembership(ctx, "e", "t1", "u1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_AddTeamMember(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships/u1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{ + "login": "u1", + "id": 5 + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.AddTeamMember(ctx, "e", "t1", "u1") + if err != nil { + t.Fatalf("AddTeamMember returned error: %v", err) + } + + want := &User{ + Login: Ptr("u1"), + ID: Ptr(int64(5)), + } + + if !cmp.Equal(got, want) { + t.Errorf("AddTeamMember = %+v, want %+v", got, want) + } + + const methodName = "AddTeamMember" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.AddTeamMember(ctx, "\n", "t1", "u1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.AddTeamMember(ctx, "e", "t1", "u1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_RemoveTeamMember(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/memberships/u1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Enterprise.RemoveTeamMember(ctx, "e", "t1", "u1") + if err != nil { + t.Fatalf("RemoveTeamMember returned error: %v", err) + } + if resp == nil { + t.Fatal("RemoveTeamMember returned nil Response") + } + + const methodName = "RemoveTeamMember" + testBadOptions(t, methodName, func() error { + _, err := client.Enterprise.RemoveTeamMember(ctx, "\n", "t1", "u1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.RemoveTeamMember(ctx, "e", "t1", "u1") + }) +} From b7fec345232051b32bf2a40de2c00eb3cb2ec3a9 Mon Sep 17 00:00:00 2001 From: JiayangZhou <47085823+JiayangZhou@users.noreply.github.com> Date: Thu, 11 Dec 2025 04:20:11 -0800 Subject: [PATCH 32/49] fix!: Change `copilot_code_review` field names to match GitHub API (#3874) BREAKING CHANGE: `CopilotCodeReviewRuleParameters.ReviewNewPushes` is now `ReviewOnPush`. --- github/rules.go | 19 ++++++- github/rules_test.go | 117 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/github/rules.go b/github/rules.go index 1efb1783f92..c4786a4e708 100644 --- a/github/rules.go +++ b/github/rules.go @@ -344,6 +344,7 @@ type BranchRules struct { TagNamePattern []*PatternBranchRule Workflows []*WorkflowsBranchRule CodeScanning []*CodeScanningBranchRule + CopilotCodeReview []*CopilotCodeReviewBranchRule // Push target rules. FileExtensionRestriction []*FileExtensionRestrictionBranchRule @@ -431,6 +432,12 @@ type CodeScanningBranchRule struct { Parameters CodeScanningRuleParameters `json:"parameters"` } +// CopilotCodeReviewBranchRule represents a copilot code review branch rule. +type CopilotCodeReviewBranchRule struct { + BranchRuleMetadata + Parameters CopilotCodeReviewRuleParameters `json:"parameters"` +} + // EmptyRuleParameters represents the parameters for a rule with no options. type EmptyRuleParameters struct{} @@ -542,7 +549,7 @@ type CodeScanningRuleParameters struct { // CopilotCodeReviewRuleParameters represents the copilot_code_review rule parameters. type CopilotCodeReviewRuleParameters struct { - ReviewNewPushes bool `json:"review_new_pushes"` + ReviewOnPush bool `json:"review_on_push"` ReviewDraftPullRequests bool `json:"review_draft_pull_requests"` } @@ -1203,6 +1210,16 @@ func (r *BranchRules) UnmarshalJSON(data []byte) error { } r.CodeScanning = append(r.CodeScanning, &CodeScanningBranchRule{BranchRuleMetadata: w.BranchRuleMetadata, Parameters: *params}) + case RulesetRuleTypeCopilotCodeReview: + params := &CopilotCodeReviewRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, params); err != nil { + return err + } + } + + r.CopilotCodeReview = append(r.CopilotCodeReview, &CopilotCodeReviewBranchRule{BranchRuleMetadata: w.BranchRuleMetadata, Parameters: *params}) } } diff --git a/github/rules_test.go b/github/rules_test.go index 6e5e1691707..ebde498da48 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -122,7 +122,7 @@ func TestRulesetRules(t *testing.T) { }, }, CopilotCodeReview: &CopilotCodeReviewRuleParameters{ - ReviewNewPushes: true, + ReviewOnPush: true, ReviewDraftPullRequests: false, }, RepositoryCreate: &EmptyRuleParameters{}, @@ -131,7 +131,7 @@ func TestRulesetRules(t *testing.T) { RepositoryTransfer: &EmptyRuleParameters{}, RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, + `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, { "all_rules_with_all_params", @@ -243,7 +243,7 @@ func TestRulesetRules(t *testing.T) { }, }, CopilotCodeReview: &CopilotCodeReviewRuleParameters{ - ReviewNewPushes: true, + ReviewOnPush: true, ReviewDraftPullRequests: false, }, RepositoryCreate: &EmptyRuleParameters{}, @@ -252,7 +252,7 @@ func TestRulesetRules(t *testing.T) { RepositoryTransfer: &EmptyRuleParameters{}, RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, + `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":false}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, } @@ -314,11 +314,11 @@ func TestRulesetRules(t *testing.T) { }{ { "invalid_copilot_code_review_bool", - `[{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}]`, + `[{"type":"copilot_code_review","parameters":{"review_on_push":"invalid_bool"}}]`, }, { "invalid_copilot_code_review_draft_pr", - `[{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}]`, + `[{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":"not_a_bool"}}]`, }, { "invalid_copilot_code_review_parameters", @@ -659,8 +659,21 @@ func TestBranchRules(t *testing.T) { }, }, }, + CopilotCodeReview: []*CopilotCodeReviewBranchRule{ + { + BranchRuleMetadata: BranchRuleMetadata{ + RulesetSourceType: RulesetSourceTypeRepository, + RulesetSource: "test/test", + RulesetID: 1, + }, + Parameters: CopilotCodeReviewRuleParameters{ + ReviewOnPush: true, + ReviewDraftPullRequests: false, + }, + }, + }, }, - `[{"type":"creation","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"update","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"required_linear_history","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"merge_queue","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"pull_request","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"commit_message_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"max_file_size":1024}},{"type":"workflows","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}]`, + `[{"type":"creation","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"update","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"required_linear_history","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"merge_queue","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"pull_request","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1},{"type":"commit_message_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"max_file_size":1024}},{"type":"workflows","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"copilot_code_review","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":{"review_on_push":true,"review_draft_pull_requests":false}}]`, }, } @@ -688,6 +701,31 @@ func TestBranchRules(t *testing.T) { }) } }) + + t.Run("UnmarshalJSON_Error", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + }{ + { + "invalid_copilot_code_review_parameters", + `[{"type":"copilot_code_review","ruleset_source_type":"Repository","ruleset_source":"test/test","ruleset_id":1,"parameters":"not_an_object"}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := &BranchRules{} + err := json.Unmarshal([]byte(tt.json), got) + if err == nil { + t.Errorf("Expected error unmarshaling %q, got nil", tt.json) + } + }) + } + }) } func TestRepositoryRule(t *testing.T) { @@ -972,11 +1010,11 @@ func TestRepositoryRule(t *testing.T) { &RepositoryRule{ Type: RulesetRuleTypeCopilotCodeReview, Parameters: &CopilotCodeReviewRuleParameters{ - ReviewNewPushes: true, + ReviewOnPush: true, ReviewDraftPullRequests: false, }, }, - `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}}`, + `{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":false}}`, }, { "copilot_code_review_empty_params", @@ -984,7 +1022,7 @@ func TestRepositoryRule(t *testing.T) { Type: RulesetRuleTypeCopilotCodeReview, Parameters: &CopilotCodeReviewRuleParameters{}, }, - `{"type":"copilot_code_review","parameters":{"review_new_pushes":false,"review_draft_pull_requests":false}}`, + `{"type":"copilot_code_review","parameters":{"review_on_push":false,"review_draft_pull_requests":false}}`, }, { "repository_create", @@ -1025,6 +1063,61 @@ func TestRepositoryRule(t *testing.T) { }, } + marshalTests := []struct { + name string + rule *RepositoryRule + json string + }{ + { + "creation", + &RepositoryRule{Type: RulesetRuleTypeCreation, Parameters: nil}, + `{"type":"creation"}`, + }, + { + "copilot_code_review", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{ + ReviewOnPush: true, + ReviewDraftPullRequests: false, + }, + }, + `{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":false}}`, + }, + { + "copilot_code_review_empty_params", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{}, + }, + `{"type":"copilot_code_review","parameters":{"review_on_push":false,"review_draft_pull_requests":false}}`, + }, + } + + t.Run("MarshalJSON", func(t *testing.T) { + t.Parallel() + + for _, test := range marshalTests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + got, err := json.Marshal(test.rule) + if err != nil { + t.Errorf("Unable to marshal JSON for %#v", test.rule) + } + + if diff := cmp.Diff(test.json, string(got)); diff != "" { + t.Errorf( + "json.Marshal returned:\n%v\nwant:\n%v\ndiff:\n%v", + string(got), + test.json, + diff, + ) + } + }) + } + }) + t.Run("UnmarshalJSON", func(t *testing.T) { t.Parallel() @@ -1059,11 +1152,11 @@ func TestRepositoryRule(t *testing.T) { }{ { "invalid_copilot_code_review_bool", - `{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}`, + `{"type":"copilot_code_review","parameters":{"review_on_push":"invalid_bool"}}`, }, { "invalid_copilot_code_review_draft_pr", - `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}`, + `{"type":"copilot_code_review","parameters":{"review_on_push":true,"review_draft_pull_requests":"not_a_bool"}}`, }, { "invalid_copilot_code_review_parameters", From c392e6f5f11d09e617451f7785d6e7fd291807d0 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 11 Dec 2025 15:14:53 +0200 Subject: [PATCH 33/49] chore: Simplify JSON marshaling for RepositoryRulesetRules (#3875) --- github/rules.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/github/rules.go b/github/rules.go index c4786a4e708..420f4feb509 100644 --- a/github/rules.go +++ b/github/rules.go @@ -580,9 +580,7 @@ type repositoryRulesetRuleWrapper struct { // MarshalJSON is a custom JSON marshaler for RulesetRules. func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { - // The RepositoryRulesetRules type marshals to between 1 and 22 rules. - // If new rules are added to RepositoryRulesetRules the capacity below needs increasing - rawRules := make([]json.RawMessage, 0, 22) + var rawRules []json.RawMessage if r.Creation != nil { bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeCreation, r.Creation) @@ -800,6 +798,10 @@ func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { rawRules = append(rawRules, json.RawMessage(bytes)) } + if len(rawRules) == 0 { + return []byte("[]"), nil + } + return json.Marshal(rawRules) } From 59f33d92970f41ebb59baae94efad147470ac81e Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 11 Dec 2025 19:21:02 +0530 Subject: [PATCH 34/49] feat: Add support for Enterprise Team Organizations APIs (#3876) --- github/enterprise_team.go | 135 +++++++++++++++++ github/enterprise_team_test.go | 267 +++++++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+) diff --git a/github/enterprise_team.go b/github/enterprise_team.go index 716a330e281..5bcce7c0fb3 100644 --- a/github/enterprise_team.go +++ b/github/enterprise_team.go @@ -283,3 +283,138 @@ func (s *EnterpriseService) RemoveTeamMember(ctx context.Context, enterprise, en return resp, nil } + +// ListAssignments gets all organizations assigned to an enterprise team. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#get-organization-assignments +// +//meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/organizations +func (s *EnterpriseService) ListAssignments(ctx context.Context, enterprise, enterpriseTeam string, opt *ListOptions) ([]*Organization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations", enterprise, enterpriseTeam) + u, err := addOptions(u, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var orgs []*Organization + resp, err := s.client.Do(ctx, req, &orgs) + if err != nil { + return nil, resp, err + } + + return orgs, resp, nil +} + +// AddMultipleAssignments assigns an enterprise team to multiple organizations. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#add-organization-assignments +// +//meta:operation POST /enterprises/{enterprise}/teams/{enterprise-team}/organizations/add +func (s *EnterpriseService) AddMultipleAssignments(ctx context.Context, enterprise, enterpriseTeam string, organizationSlugs []string) ([]*Organization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations/add", enterprise, enterpriseTeam) + + req, err := s.client.NewRequest("POST", u, map[string][]string{"organization_slugs": organizationSlugs}) + if err != nil { + return nil, nil, err + } + + var orgs []*Organization + resp, err := s.client.Do(ctx, req, &orgs) + if err != nil { + return nil, resp, err + } + + return orgs, resp, nil +} + +// RemoveMultipleAssignments unassigns an enterprise team from multiple organizations. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#remove-organization-assignments +// +//meta:operation POST /enterprises/{enterprise}/teams/{enterprise-team}/organizations/remove +func (s *EnterpriseService) RemoveMultipleAssignments(ctx context.Context, enterprise, enterpriseTeam string, organizationSlugs []string) ([]*Organization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations/remove", enterprise, enterpriseTeam) + + req, err := s.client.NewRequest("POST", u, map[string][]string{"organization_slugs": organizationSlugs}) + if err != nil { + return nil, nil, err + } + + var orgs []*Organization + resp, err := s.client.Do(ctx, req, &orgs) + if err != nil { + return nil, resp, err + } + + return orgs, resp, nil +} + +// GetAssignment checks if an enterprise team is assigned to an organization. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#get-organization-assignment +// +//meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/organizations/{org} +func (s *EnterpriseService) GetAssignment(ctx context.Context, enterprise, enterpriseTeam, org string) (*Organization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations/%v", enterprise, enterpriseTeam, org) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var organization *Organization + resp, err := s.client.Do(ctx, req, &organization) + if err != nil { + return nil, resp, err + } + + return organization, resp, nil +} + +// AddAssignment assigns an enterprise team to an organizations. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#add-an-organization-assignment +// +//meta:operation PUT /enterprises/{enterprise}/teams/{enterprise-team}/organizations/{org} +func (s *EnterpriseService) AddAssignment(ctx context.Context, enterprise, enterpriseTeam, org string) (*Organization, *Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations/%v", enterprise, enterpriseTeam, org) + + req, err := s.client.NewRequest("PUT", u, nil) + if err != nil { + return nil, nil, err + } + + var organization *Organization + resp, err := s.client.Do(ctx, req, &organization) + if err != nil { + return nil, resp, err + } + + return organization, resp, nil +} + +// RemoveAssignment unassigns an enterprise team from an organizations. +// +// GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#delete-an-organization-assignment +// +//meta:operation DELETE /enterprises/{enterprise}/teams/{enterprise-team}/organizations/{org} +func (s *EnterpriseService) RemoveAssignment(ctx context.Context, enterprise, enterpriseTeam, org string) (*Response, error) { + u := fmt.Sprintf("enterprises/%v/teams/%v/organizations/%v", enterprise, enterpriseTeam, org) + + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/enterprise_team_test.go b/github/enterprise_team_test.go index a31872653d3..c300d4affbf 100644 --- a/github/enterprise_team_test.go +++ b/github/enterprise_team_test.go @@ -479,3 +479,270 @@ func TestEnterpriseService_RemoveTeamMember(t *testing.T) { return client.Enterprise.RemoveTeamMember(ctx, "e", "t1", "u1") }) } + +func TestEnterpriseService_ListAssignments(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "login": "team-one", + "id": 1, + "node_id": "node-id", + "url": "https://example.com/team1", + "repos_url": "https://example.com/members", + "events_url": "https://example.com/events", + "hooks_url": "https://api.github.com/orgs/team-one/hooks", + "issues_url": "https://api.github.com/orgs/team-one/issues", + "members_url": "https://api.github.com/orgs/team-one/members", + "public_members_url": "https://api.github.com/orgs/team-one/public_members", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "description": "Team One" + } + ]`) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + got, _, err := client.Enterprise.ListAssignments(ctx, "e", "t1", opts) + if err != nil { + t.Fatalf("Enterprise.ListAssignments returned error: %v", err) + } + + want := []*Organization{ + { + Login: Ptr("team-one"), + URL: Ptr("https://example.com/team1"), + NodeID: Ptr("node-id"), + ReposURL: Ptr("https://example.com/members"), + EventsURL: Ptr("https://example.com/events"), + ID: Ptr(int64(1)), + HooksURL: Ptr("https://api.github.com/orgs/team-one/hooks"), + IssuesURL: Ptr("https://api.github.com/orgs/team-one/issues"), + MembersURL: Ptr("https://api.github.com/orgs/team-one/members"), + PublicMembersURL: Ptr("https://api.github.com/orgs/team-one/public_members"), + AvatarURL: Ptr("https://github.com/images/error/octocat_happy.gif"), + Description: Ptr("Team One"), + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Enterprise.ListAssignments = %+v, want %+v", got, want) + } + + const methodName = "ListAssignments" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.ListAssignments(ctx, "\n", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListAssignments(ctx, "e", "t1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_AddMultipleAssignments(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations/add", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + fmt.Fprint(w, `[{ + "login": "o1", + "id": 1 + },{ + "login": "o2", + "id": 2 + }]`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.AddMultipleAssignments(ctx, "e", "t1", []string{"o1", "o2"}) + if err != nil { + t.Fatalf("AddMultipleAssignments returned error: %v", err) + } + + want := []*Organization{ + {Login: Ptr("o1"), ID: Ptr(int64(1))}, + {Login: Ptr("o2"), ID: Ptr(int64(2))}, + } + + if !cmp.Equal(got, want) { + t.Errorf("AddMultipleAssignments = %+v, want %+v", got, want) + } + + const methodName = "AddMultipleAssignments" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.AddMultipleAssignments(ctx, "\n", "t1", []string{"o1"}) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.AddMultipleAssignments(ctx, "e", "t1", []string{"o1"}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_RemoveMultipleAssignments(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations/remove", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + + fmt.Fprint(w, `[{ + "login": "o1", + "id": 1 + },{ + "login": "o2", + "id": 2 + }]`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.RemoveMultipleAssignments(ctx, "e", "t1", []string{"o1", "o2"}) + if err != nil { + t.Fatalf("RemoveMultipleAssignments returned error: %v", err) + } + + want := []*Organization{ + {Login: Ptr("o1"), ID: Ptr(int64(1))}, + {Login: Ptr("o2"), ID: Ptr(int64(2))}, + } + + if !cmp.Equal(got, want) { + t.Errorf("RemoveMultipleAssignments = %+v, want %+v", got, want) + } + + const methodName = "RemoveMultipleAssignments" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.RemoveMultipleAssignments(ctx, "\n", "t1", []string{"o1"}) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.RemoveMultipleAssignments(ctx, "e", "t1", []string{"o1"}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_GetAssignment(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations/o1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "login": "o1", + "id": 10 + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.GetAssignment(ctx, "e", "t1", "o1") + if err != nil { + t.Fatalf("GetAssignment returned error: %v", err) + } + + want := &Organization{ + Login: Ptr("o1"), + ID: Ptr(int64(10)), + } + + if !cmp.Equal(got, want) { + t.Errorf("GetAssignment = %+v, want %+v", got, want) + } + + const methodName = "GetAssignment" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.GetAssignment(ctx, "\n", "t1", "o1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetAssignment(ctx, "e", "t1", "o1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_AddAssignment(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations/o1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{ + "login": "o1", + "id": 5 + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.AddAssignment(ctx, "e", "t1", "o1") + if err != nil { + t.Fatalf("AddAssignment returned error: %v", err) + } + + want := &Organization{ + Login: Ptr("o1"), + ID: Ptr(int64(5)), + } + + if !cmp.Equal(got, want) { + t.Errorf("AddAssignment = %+v, want %+v", got, want) + } + + const methodName = "AddAssignment" + testBadOptions(t, methodName, func() error { + _, _, err := client.Enterprise.AddAssignment(ctx, "\n", "t1", "o1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.AddAssignment(ctx, "e", "t1", "o1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_RemoveAssignment(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/teams/t1/organizations/o1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Enterprise.RemoveAssignment(ctx, "e", "t1", "o1") + if err != nil { + t.Fatalf("RemoveAssignment returned error: %v", err) + } + if resp == nil { + t.Fatal("RemoveAssignment returned nil Response") + } + + const methodName = "RemoveAssignment" + testBadOptions(t, methodName, func() error { + _, err := client.Enterprise.RemoveAssignment(ctx, "\n", "t1", "o1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.RemoveAssignment(ctx, "e", "t1", "o1") + }) +} From 07ddcd9c1b275afc5147c028328099596e00b958 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Fri, 12 Dec 2025 14:53:44 +0000 Subject: [PATCH 35/49] docs: Clarify `CreateTree` semantics and `TreeEntry` usage (#3877) --- github/git_trees.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/github/git_trees.go b/github/git_trees.go index 2b701a3c658..2ff0dc592d8 100644 --- a/github/git_trees.go +++ b/github/git_trees.go @@ -30,6 +30,13 @@ func (t Tree) String() string { // TreeEntry represents the contents of a tree structure. TreeEntry can // represent either a blob, a commit (in the case of a submodule), or another // tree. +// +// When used with [GitService.CreateTree], set Content for small text files, +// or set SHA to reference an existing blob (use [GitService.CreateBlob] for +// binary files or large content). To delete an entry, set both Content and SHA +// to nil; the entry will be serialized with `"sha": null` which the API interprets +// as a deletion. When deleting, the Type and Mode fields are ignored; only Path +// is required. type TreeEntry struct { SHA *string `json:"sha,omitempty"` Path *string `json:"path,omitempty"` @@ -127,6 +134,12 @@ type createTree struct { // path modifying that tree are specified, it will overwrite the contents of // that tree with the new path contents and write a new tree out. // +// When baseTree is provided, entries are merged with that tree: paths not +// mentioned in entries are preserved from the base tree. If the same path +// appears multiple times in entries, the last entry wins. To delete an entry, +// include a [TreeEntry] with the path and both SHA and Content set to nil. +// Entire directories can be deleted this way. +// // GitHub API docs: https://docs.github.com/rest/git/trees#create-a-tree // //meta:operation POST /repos/{owner}/{repo}/git/trees From 8ca48ddef11709004ffb8b19dcbf34763c4ec0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C5=BDi=C5=BEkovsk=C3=BD?= Date: Sat, 13 Dec 2025 19:09:50 +0100 Subject: [PATCH 36/49] docs: Reformulate deprecation notice for `Commits` field in `PushEvent` (#3880) --- github/event_types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github/event_types.go b/github/event_types.go index 178df686f2f..eb1fd57a9a3 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1374,7 +1374,8 @@ type PushEvent struct { Size *int `json:"size,omitempty"` // Commits is the list of commits in the push event. // - // Deprecated: GitHub will remove commit summaries from Events API payloads on October 7, 2025. + // This field is only populated for webhook events. + // It has been removed from Events API payloads on October 7, 2025. // Use the Commits REST API endpoint to get detailed commit information. // See: https://docs.github.com/rest/commits/commits#list-commits Commits []*HeadCommit `json:"commits,omitempty"` From fe6896d599890a8d7a8ed21651b8e889010c2f2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:11:47 -0500 Subject: [PATCH 37/49] build(deps): Bump actions/cache from 4.3.0 to 5.0.1 in the actions group (#3883) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c87fde4c96b..556764829b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,7 +54,7 @@ jobs: echo "go-mod-cache=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT - name: Cache go modules - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: | ${{ steps.cache-paths.outputs.go-cache }} From 39bba332fd3f841a8f5ad36f207e4a9d7c8e21aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:17:11 -0500 Subject: [PATCH 38/49] build(deps): Bump golang.org/x/net from 0.47.0 to 0.48.0 in /scrape (#3882) --- scrape/go.mod | 2 +- scrape/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scrape/go.mod b/scrape/go.mod index 4a3094bb570..1b9f8b0b6a5 100644 --- a/scrape/go.mod +++ b/scrape/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/go-github/v80 v80.0.0 github.com/xlzd/gotp v0.1.0 - golang.org/x/net v0.47.0 + golang.org/x/net v0.48.0 ) require ( diff --git a/scrape/go.sum b/scrape/go.sum index 50fd096e667..4bcb802597c 100644 --- a/scrape/go.sum +++ b/scrape/go.sum @@ -33,8 +33,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From afaf7a6da731ff6aa682d5662bcb379ad7e73b29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:22:34 -0500 Subject: [PATCH 39/49] build(deps): Bump codecov/codecov-action from 5.5.1 to 5.5.2 (#3884) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 556764829b1..01d82273570 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,6 +76,6 @@ jobs: - name: Upload coverage to Codecov if: ${{ matrix.update-coverage }} - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: use_oidc: true From e1238d060fd70516ae3741108eaee37bc9a103b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:25:39 -0500 Subject: [PATCH 40/49] build(deps): Bump golang.org/x/crypto from 0.45.0 to 0.46.0 in /example (#3885) --- example/go.mod | 8 ++++---- example/go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example/go.mod b/example/go.mod index 89d812429be..9c00c368e17 100644 --- a/example/go.mod +++ b/example/go.mod @@ -9,7 +9,7 @@ require ( github.com/gofri/go-github-ratelimit/v2 v2.0.2 github.com/google/go-github/v80 v80.0.0 github.com/sigstore/sigstore-go v0.6.1 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.46.0 golang.org/x/term v0.38.0 google.golang.org/appengine v1.6.8 ) @@ -87,11 +87,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect - golang.org/x/mod v0.29.0 // indirect + golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/example/go.sum b/example/go.sum index cc2b7ccd6bf..4b9131a17fa 100644 --- a/example/go.sum +++ b/example/go.sum @@ -355,13 +355,13 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -371,8 +371,8 @@ golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -388,8 +388,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From ecab7f192f4f976579a52e83ed69cd94855243b4 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 17 Dec 2025 20:49:01 +0200 Subject: [PATCH 41/49] chore: Rename 'opt' to 'opts' in multiple methods (#3887) --- github/enterprise_team.go | 12 ++++++------ github/orgs_issue_types.go | 8 ++++---- github/security_advisories.go | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/github/enterprise_team.go b/github/enterprise_team.go index 5bcce7c0fb3..e9317b0772b 100644 --- a/github/enterprise_team.go +++ b/github/enterprise_team.go @@ -43,9 +43,9 @@ type EnterpriseTeamCreateOrUpdateRequest struct { // GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-teams#list-enterprise-teams // //meta:operation GET /enterprises/{enterprise}/teams -func (s *EnterpriseService) ListTeams(ctx context.Context, enterprise string, opt *ListOptions) ([]*EnterpriseTeam, *Response, error) { +func (s *EnterpriseService) ListTeams(ctx context.Context, enterprise string, opts *ListOptions) ([]*EnterpriseTeam, *Response, error) { u := fmt.Sprintf("enterprises/%v/teams", enterprise) - u, err := addOptions(u, opt) + u, err := addOptions(u, opts) if err != nil { return nil, nil, err } @@ -156,9 +156,9 @@ func (s *EnterpriseService) DeleteTeam(ctx context.Context, enterprise, teamSlug // GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#list-members-in-an-enterprise-team // //meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships -func (s *EnterpriseService) ListTeamMembers(ctx context.Context, enterprise, enterpriseTeam string, opt *ListOptions) ([]*User, *Response, error) { +func (s *EnterpriseService) ListTeamMembers(ctx context.Context, enterprise, enterpriseTeam string, opts *ListOptions) ([]*User, *Response, error) { u := fmt.Sprintf("enterprises/%v/teams/%v/memberships", enterprise, enterpriseTeam) - u, err := addOptions(u, opt) + u, err := addOptions(u, opts) if err != nil { return nil, nil, err } @@ -289,9 +289,9 @@ func (s *EnterpriseService) RemoveTeamMember(ctx context.Context, enterprise, en // GitHub API docs: https://docs.github.com/rest/enterprise-teams/enterprise-team-organizations#get-organization-assignments // //meta:operation GET /enterprises/{enterprise}/teams/{enterprise-team}/organizations -func (s *EnterpriseService) ListAssignments(ctx context.Context, enterprise, enterpriseTeam string, opt *ListOptions) ([]*Organization, *Response, error) { +func (s *EnterpriseService) ListAssignments(ctx context.Context, enterprise, enterpriseTeam string, opts *ListOptions) ([]*Organization, *Response, error) { u := fmt.Sprintf("enterprises/%v/teams/%v/organizations", enterprise, enterpriseTeam) - u, err := addOptions(u, opt) + u, err := addOptions(u, opts) if err != nil { return nil, nil, err } diff --git a/github/orgs_issue_types.go b/github/orgs_issue_types.go index a111dce3ff9..0819c218382 100644 --- a/github/orgs_issue_types.go +++ b/github/orgs_issue_types.go @@ -46,9 +46,9 @@ func (s *OrganizationsService) ListIssueTypes(ctx context.Context, org string) ( // 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) { +func (s *OrganizationsService) CreateIssueType(ctx context.Context, org string, opts *CreateOrUpdateIssueTypesOptions) (*IssueType, *Response, error) { u := fmt.Sprintf("orgs/%v/issue-types", org) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opts) if err != nil { return nil, nil, err } @@ -67,9 +67,9 @@ func (s *OrganizationsService) CreateIssueType(ctx context.Context, org string, // 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) { +func (s *OrganizationsService) UpdateIssueType(ctx context.Context, org string, issueTypeID int64, opts *CreateOrUpdateIssueTypesOptions) (*IssueType, *Response, error) { u := fmt.Sprintf("orgs/%v/issue-types/%v", org, issueTypeID) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opts) if err != nil { return nil, nil, err } diff --git a/github/security_advisories.go b/github/security_advisories.go index 9d1e2513195..5c7b8fd4e3c 100644 --- a/github/security_advisories.go +++ b/github/security_advisories.go @@ -190,9 +190,9 @@ func (s *SecurityAdvisoriesService) CreateTemporaryPrivateFork(ctx context.Conte // GitHub API docs: https://docs.github.com/rest/security-advisories/repository-advisories#list-repository-security-advisories-for-an-organization // //meta:operation GET /orgs/{org}/security-advisories -func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisoriesForOrg(ctx context.Context, org string, opt *ListRepositorySecurityAdvisoriesOptions) ([]*SecurityAdvisory, *Response, error) { +func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisoriesForOrg(ctx context.Context, org string, opts *ListRepositorySecurityAdvisoriesOptions) ([]*SecurityAdvisory, *Response, error) { url := fmt.Sprintf("orgs/%v/security-advisories", org) - url, err := addOptions(url, opt) + url, err := addOptions(url, opts) if err != nil { return nil, nil, err } @@ -216,9 +216,9 @@ func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisoriesForOrg(ctx c // GitHub API docs: https://docs.github.com/rest/security-advisories/repository-advisories#list-repository-security-advisories // //meta:operation GET /repos/{owner}/{repo}/security-advisories -func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisories(ctx context.Context, owner, repo string, opt *ListRepositorySecurityAdvisoriesOptions) ([]*SecurityAdvisory, *Response, error) { +func (s *SecurityAdvisoriesService) ListRepositorySecurityAdvisories(ctx context.Context, owner, repo string, opts *ListRepositorySecurityAdvisoriesOptions) ([]*SecurityAdvisory, *Response, error) { url := fmt.Sprintf("repos/%v/%v/security-advisories", owner, repo) - url, err := addOptions(url, opt) + url, err := addOptions(url, opts) if err != nil { return nil, nil, err } From 2c83bd6b07e8d4a948e7d3189dbe27905b7e45df Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Fri, 19 Dec 2025 21:19:38 +0530 Subject: [PATCH 42/49] feat: Add support for remaining Codespaces APIs (#3886) --- github/codespaces.go | 335 +++++++++++++++++++++- github/codespaces_test.go | 482 ++++++++++++++++++++++++++++++++ github/github-accessors.go | 240 ++++++++++++++++ github/github-accessors_test.go | 321 +++++++++++++++++++++ 4 files changed, 1373 insertions(+), 5 deletions(-) diff --git a/github/codespaces.go b/github/codespaces.go index 608370503f6..bc8d617f34e 100644 --- a/github/codespaces.go +++ b/github/codespaces.go @@ -167,7 +167,101 @@ type CreateCodespaceOptions struct { DisplayName *string `json:"display_name,omitempty"` // RetentionPeriodMinutes represents the duration in minutes after codespace has gone idle in which it will be deleted. // Must be integer minutes between 0 and 43200 (30 days). - RetentionPeriodMinutes *int `json:"retention_period_minutes,omitempty"` + RetentionPeriodMinutes *int `json:"retention_period_minutes,omitempty"` + Location *string `json:"location,omitempty"` +} + +// DevContainer represents a devcontainer configuration in a repository. +type DevContainer struct { + Path string `json:"path"` + Name *string `json:"name,omitempty"` + DisplayName *string `json:"display_name,omitempty"` +} + +// DevContainerConfigurations represents a list of devcontainer configurations in a repository. +type DevContainerConfigurations struct { + Devcontainers []*DevContainer `json:"devcontainers"` + TotalCount int64 `json:"total_count"` +} + +// CodespaceDefaults represents default settings for a Codespace. +type CodespaceDefaults struct { + Location string `json:"location"` + DevcontainerPath *string `json:"devcontainer_path,omitempty"` +} + +// CodespaceDefaultAttributes represents the default attributes for codespaces created by the user with the repository. +type CodespaceDefaultAttributes struct { + BillableOwner *User `json:"billable_owner"` + Defaults *CodespaceDefaults `json:"defaults"` +} + +// CodespaceGetDefaultAttributesOptions represents options for getting default attributes for a codespace. +type CodespaceGetDefaultAttributesOptions struct { + // Ref represents the branch or commit to check for a default devcontainer path. If not specified, the default branch will be checked. + Ref *string `url:"ref,omitempty"` + // ClientIP represents an alternative IP for default location auto-detection, such as when proxying a request. + ClientIP *string `url:"client_ip,omitempty"` +} + +// CodespacePullRequestOptions represents options for a CodespacePullRequest. +type CodespacePullRequestOptions struct { + // PullRequestNumber represents the pull request number. + PullRequestNumber int64 `json:"pull_request_number"` + // RepositoryID represents the repository ID for this codespace. + RepositoryID int64 `json:"repository_id"` +} + +// CodespaceCreateForUserOptions represents options for creating a codespace for the authenticated user. +type CodespaceCreateForUserOptions struct { + PullRequest *CodespacePullRequestOptions `json:"pull_request"` + // RepositoryID represents the repository ID for this codespace. + RepositoryID int64 `json:"repository_id"` + Ref *string `json:"ref,omitempty"` + Geo *string `json:"geo,omitempty"` + ClientIP *string `json:"client_ip,omitempty"` + RetentionPeriodMinutes *int `json:"retention_period_minutes,omitempty"` + Location *string `json:"location,omitempty"` + Machine *string `json:"machine,omitempty"` + DevcontainerPath *string `json:"devcontainer_path,omitempty"` + MultiRepoPermissionsOptOut *bool `json:"multi_repo_permissions_opt_out,omitempty"` + WorkingDirectory *string `json:"working_directory,omitempty"` + IdleTimeoutMinutes *int `json:"idle_timeout_minutes,omitempty"` + DisplayName *string `json:"display_name,omitempty"` +} + +// UpdateCodespaceOptions represents options for updating a codespace. +type UpdateCodespaceOptions struct { + // Machine represents a valid machine to transition this codespace to. + Machine *string `json:"machine,omitempty"` + // RecentFolders represents the recently opened folders inside the codespace. + // It is currently used by the clients to determine the folder path to load the codespace in. + RecentFolders []string `json:"recent_folders,omitempty"` +} + +// CodespaceExport represents an export of a codespace. +type CodespaceExport struct { + // Can be one of: `succeeded`, `failed`, `in_progress`. + State *string `json:"state,omitempty"` + CompletedAt *Timestamp `json:"completed_at,omitempty"` + Branch *string `json:"branch,omitempty"` + SHA *string `json:"sha,omitempty"` + ID *string `json:"id,omitempty"` + ExportURL *string `json:"export_url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` +} + +// PublishCodespaceOptions represents options for creating a repository from an unpublished codespace. +type PublishCodespaceOptions struct { + // Name represents the name of the new repository. + Name *string `json:"name,omitempty"` + // Private represents whether the new repository is private. Defaults to false. + Private *bool `json:"private,omitempty"` +} + +// CodespacePermissions represents a response indicating whether the permissions defined by a devcontainer have been accepted. +type CodespacePermissions struct { + Accepted bool `json:"accepted"` } // CreateInRepo creates a codespace in a repository. @@ -181,7 +275,6 @@ type CreateCodespaceOptions struct { //meta:operation POST /repos/{owner}/{repo}/codespaces func (s *CodespacesService) CreateInRepo(ctx context.Context, owner, repo string, request *CreateCodespaceOptions) (*Codespace, *Response, error) { u := fmt.Sprintf("repos/%v/%v/codespaces", owner, repo) - req, err := s.client.NewRequest("POST", u, request) if err != nil { return nil, nil, err @@ -206,7 +299,6 @@ func (s *CodespacesService) CreateInRepo(ctx context.Context, owner, repo string //meta:operation POST /user/codespaces/{codespace_name}/start func (s *CodespacesService) Start(ctx context.Context, codespaceName string) (*Codespace, *Response, error) { u := fmt.Sprintf("user/codespaces/%v/start", codespaceName) - req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, nil, err @@ -231,7 +323,6 @@ func (s *CodespacesService) Start(ctx context.Context, codespaceName string) (*C //meta:operation POST /user/codespaces/{codespace_name}/stop func (s *CodespacesService) Stop(ctx context.Context, codespaceName string) (*Codespace, *Response, error) { u := fmt.Sprintf("user/codespaces/%v/stop", codespaceName) - req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, nil, err @@ -256,7 +347,6 @@ func (s *CodespacesService) Stop(ctx context.Context, codespaceName string) (*Co //meta:operation DELETE /user/codespaces/{codespace_name} func (s *CodespacesService) Delete(ctx context.Context, codespaceName string) (*Response, error) { u := fmt.Sprintf("user/codespaces/%v", codespaceName) - req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return nil, err @@ -264,3 +354,238 @@ func (s *CodespacesService) Delete(ctx context.Context, codespaceName string) (* return s.client.Do(ctx, req, nil) } + +// ListDevContainerConfigurations lists devcontainer configurations in a repository for the authenticated user. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#list-devcontainer-configurations-in-a-repository-for-the-authenticated-user +// +//meta:operation GET /repos/{owner}/{repo}/codespaces/devcontainers +func (s *CodespacesService) ListDevContainerConfigurations(ctx context.Context, owner, repo string, opts *ListOptions) (*DevContainerConfigurations, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/codespaces/devcontainers", owner, repo) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var devcontainers *DevContainerConfigurations + resp, err := s.client.Do(ctx, req, &devcontainers) + if err != nil { + return nil, resp, err + } + + return devcontainers, resp, nil +} + +// GetDefaultAttributes gets the default attributes for codespaces created by the user with the repository. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#get-default-attributes-for-a-codespace +// +//meta:operation GET /repos/{owner}/{repo}/codespaces/new +func (s *CodespacesService) GetDefaultAttributes(ctx context.Context, owner, repo string, opts *CodespaceGetDefaultAttributesOptions) (*CodespaceDefaultAttributes, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/codespaces/new", owner, repo) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var attributes *CodespaceDefaultAttributes + resp, err := s.client.Do(ctx, req, &attributes) + if err != nil { + return nil, resp, err + } + + return attributes, resp, nil +} + +// CheckPermissions checks whether the permissions defined by a given devcontainer configuration have been accepted by the authenticated user. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#check-if-permissions-defined-by-a-devcontainer-have-been-accepted-by-the-authenticated-user +// +//meta:operation GET /repos/{owner}/{repo}/codespaces/permissions_check +func (s *CodespacesService) CheckPermissions(ctx context.Context, owner, repo, ref, devcontainerPath string) (*CodespacePermissions, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/codespaces/permissions_check", owner, repo) + u, err := addOptions(u, &struct { + Ref string `url:"ref"` + DevcontainerPath string `url:"devcontainer_path"` + }{ + Ref: ref, + DevcontainerPath: devcontainerPath, + }) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var permissions *CodespacePermissions + resp, err := s.client.Do(ctx, req, &permissions) + if err != nil { + return nil, resp, err + } + + return permissions, resp, nil +} + +// CreateFromPullRequest creates a codespace owned by the authenticated user for the specified pull request. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#create-a-codespace-from-a-pull-request +// +//meta:operation POST /repos/{owner}/{repo}/pulls/{pull_number}/codespaces +func (s *CodespacesService) CreateFromPullRequest(ctx context.Context, owner, repo string, pullNumber int, request *CreateCodespaceOptions) (*Codespace, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/%v/codespaces", owner, repo, pullNumber) + req, err := s.client.NewRequest("POST", u, request) + if err != nil { + return nil, nil, err + } + + var codespace *Codespace + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// Create creates a new codespace, owned by the authenticated user. +// +// This method requires either RepositoryId OR a PullRequest but not both. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#create-a-codespace-for-the-authenticated-user +// +//meta:operation POST /user/codespaces +func (s *CodespacesService) Create(ctx context.Context, opts *CodespaceCreateForUserOptions) (*Codespace, *Response, error) { + u := "user/codespaces" + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + + var codespace *Codespace + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// Get gets information about a user's codespace. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#get-a-codespace-for-the-authenticated-user +// +//meta:operation GET /user/codespaces/{codespace_name} +func (s *CodespacesService) Get(ctx context.Context, codespaceName string) (*Codespace, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v", codespaceName) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var codespace *Codespace + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// Update updates a codespace owned by the authenticated user. +// +// Only the codespace's machine type and recent folders can be modified using this endpoint. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#update-a-codespace-for-the-authenticated-user +// +//meta:operation PATCH /user/codespaces/{codespace_name} +func (s *CodespacesService) Update(ctx context.Context, codespaceName string, opts *UpdateCodespaceOptions) (*Codespace, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v", codespaceName) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + + var codespace *Codespace + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// ExportCodespace triggers an export of the specified codespace and returns a URL and ID where the status of the export can be monitored. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#export-a-codespace-for-the-authenticated-user +// +//meta:operation POST /user/codespaces/{codespace_name}/exports +func (s *CodespacesService) ExportCodespace(ctx context.Context, codespaceName string) (*CodespaceExport, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v/exports", codespaceName) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, nil, err + } + + var codespace *CodespaceExport + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// GetLatestCodespaceExport gets information about an export of a codespace. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#get-details-about-a-codespace-export +// +//meta:operation GET /user/codespaces/{codespace_name}/exports/{export_id} +func (s *CodespacesService) GetLatestCodespaceExport(ctx context.Context, codespaceName string) (*CodespaceExport, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v/exports/latest", codespaceName) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var codespace *CodespaceExport + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} + +// Publish publishes an unpublished codespace, creating a new repository and assigning it to the codespace. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/codespaces#create-a-repository-from-an-unpublished-codespace +// +//meta:operation POST /user/codespaces/{codespace_name}/publish +func (s *CodespacesService) Publish(ctx context.Context, codespaceName string, opts *PublishCodespaceOptions) (*Codespace, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v/publish", codespaceName) + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + + var codespace *Codespace + resp, err := s.client.Do(ctx, req, &codespace) + if err != nil { + return nil, resp, err + } + + return codespace, resp, nil +} diff --git a/github/codespaces_test.go b/github/codespaces_test.go index eb3d452326e..9bffbf1a1ca 100644 --- a/github/codespaces_test.go +++ b/github/codespaces_test.go @@ -291,3 +291,485 @@ func TestCodespacesService_Delete(t *testing.T) { return client.Codespaces.Delete(ctx, "codespace_1") }) } + +func TestCodespacesService_ListDevContainerConfigurations(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/codespaces/devcontainers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "total_count": 1, + "devcontainers": [{ + "path": ".devcontainer/foobar/devcontainer.json", + "name": "foobar", + "display_name": "foobar" + }] + }`) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 10} + + got, _, err := client.Codespaces.ListDevContainerConfigurations(ctx, "o", "r", opts) + if err != nil { + t.Fatalf("Codespaces.ListDevContainerConfigurations returned error: %v", err) + } + + want := &DevContainerConfigurations{ + TotalCount: 1, + Devcontainers: []*DevContainer{ + { + Path: ".devcontainer/foobar/devcontainer.json", + Name: Ptr("foobar"), + DisplayName: Ptr("foobar"), + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListDevContainerConfigurations = %+v, want %+v", got, want) + } + + const methodName = "ListDevContainerConfigurations" + + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListDevContainerConfigurations(ctx, "\n", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListDevContainerConfigurations(ctx, "e", "r", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_GetDefaultAttributes(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/codespaces/new", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "billable_owner": { + "login": "user1", + "id": 1001, + "url": "https://example.com/user1" + }, + "defaults": { + "devcontainer_path": ".devcontainer/devcontainer.json", + "location": "WestUs2" + } + }`) + }) + + ctx := t.Context() + + opt := &CodespaceGetDefaultAttributesOptions{ + Ref: Ptr("main"), + ClientIP: Ptr("1.2.3.4"), + } + + got, _, err := client.Codespaces.GetDefaultAttributes(ctx, "o", "r", opt) + if err != nil { + t.Fatalf("Codespaces.GetDefaultAttributes returned error: %v", err) + } + + want := &CodespaceDefaultAttributes{ + BillableOwner: &User{ + Login: Ptr("user1"), + ID: Ptr(int64(1001)), + URL: Ptr("https://example.com/user1"), + }, + Defaults: &CodespaceDefaults{ + DevcontainerPath: Ptr(".devcontainer/devcontainer.json"), + Location: "WestUs2", + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.GetDefaultAttributes = %+v, want %+v", got, want) + } + + const methodName = "GetDefaultAttributes" + + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.GetDefaultAttributes(ctx, "\n", "\n", opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.GetDefaultAttributes(ctx, "e", "r", opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_CheckPermissions(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/codespaces/permissions_check", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"accepted": true}`) + }) + + ctx := t.Context() + hasPermission, _, err := client.Codespaces.CheckPermissions(ctx, "o", "r", "main", "path") + if err != nil { + t.Errorf("Codespaces.CheckPermissions returned error: %v", err) + } + + want := CodespacePermissions{Accepted: true} + if !cmp.Equal(hasPermission, &want) { + t.Errorf("Codespaces.CheckPermissions = %+v, want %+v", hasPermission, want) + } + + const methodName = "CheckPermissions" + + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.CheckPermissions(ctx, "\n", "\n", "main", "path") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.CheckPermissions(ctx, "o", "r", "main", "path") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_CreateFromPullRequest(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/owner/repo/pulls/42/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"machine":"standardLinux","idle_timeout_minutes":60}`+"\n") + fmt.Fprint(w, `{"id":1, "repository": {"id": 1}}`) + }) + input := &CreateCodespaceOptions{ + Machine: Ptr("standardLinux"), + IdleTimeoutMinutes: Ptr(60), + } + ctx := t.Context() + codespace, _, err := client.Codespaces.CreateFromPullRequest(ctx, "owner", "repo", 42, input) + if err != nil { + t.Errorf("Codespaces.CreateFromPullRequest returned error: %v", err) + } + want := &Codespace{ + ID: Ptr(int64(1)), + Repository: &Repository{ + ID: Ptr(int64(1)), + }, + } + + if !cmp.Equal(codespace, want) { + t.Errorf("Codespaces.CreateFromPullRequest returned %+v, want %+v", codespace, want) + } + + const methodName = "CreateFromPullRequest" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Codespaces.CreateFromPullRequest(ctx, "\n", "", 0, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.CreateFromPullRequest(ctx, "o", "r", 42, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_Create(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody( + t, + r, + `{"pull_request":null,"repository_id":111,"ref":"main","geo":"WestUs2","machine":"standardLinux","idle_timeout_minutes":60}`+"\n", + ) + fmt.Fprint(w, `{"id":1,"repository":{"id":111}}`) + }) + + opt := &CodespaceCreateForUserOptions{ + Ref: Ptr("main"), + Geo: Ptr("WestUs2"), + Machine: Ptr("standardLinux"), + IdleTimeoutMinutes: Ptr(60), + RepositoryID: int64(111), + PullRequest: nil, + } + + ctx := t.Context() + codespace, _, err := client.Codespaces.Create( + ctx, + opt, + ) + if err != nil { + t.Fatalf("Codespaces.Create returned error: %v", err) + } + + want := &Codespace{ + ID: Ptr(int64(1)), + Repository: &Repository{ + ID: Ptr(int64(111)), + }, + } + + if !cmp.Equal(codespace, want) { + t.Errorf("Codespaces.Create returned %+v, want %+v", codespace, want) + } + + const methodName = "Create" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.Create( + ctx, + opt, + ) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_Get(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1,"repository":{"id":111}}`) + }) + + ctx := t.Context() + codespace, _, err := client.Codespaces.Get(ctx, "codespace_1") + if err != nil { + t.Fatalf("Codespaces.Get returned error: %v", err) + } + + want := &Codespace{ + ID: Ptr(int64(1)), + Repository: &Repository{ + ID: Ptr(int64(111)), + }, + } + + if !cmp.Equal(codespace, want) { + t.Errorf("Codespaces.Get returned %+v, want %+v", codespace, want) + } + + const methodName = "Get" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.Get(ctx, "codespace_1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_Update(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody( + t, + r, + `{"machine":"standardLinux","recent_folders":["folder1","folder2"]}`+"\n", + ) + fmt.Fprint(w, `{"id":1,"repository":{"id":111}}`) + }) + + opt := &UpdateCodespaceOptions{ + Machine: Ptr("standardLinux"), + RecentFolders: []string{ + "folder1", + "folder2", + }, + } + + ctx := t.Context() + codespace, _, err := client.Codespaces.Update( + ctx, + "codespace_1", + opt, + ) + if err != nil { + t.Fatalf("Codespaces.Update returned error: %v", err) + } + + want := &Codespace{ + ID: Ptr(int64(1)), + Repository: &Repository{ + ID: Ptr(int64(111)), + }, + } + + if !cmp.Equal(codespace, want) { + t.Errorf("Codespaces.Update returned %+v, want %+v", codespace, want) + } + + const methodName = "Update" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.Update( + ctx, + "codespace_1", + opt, + ) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_ExportCodespace(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1/exports", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{ + "state": "succeeded", + "completed_at": "2025-12-11T00:00:00Z", + "branch": "main", + "export_url": "https://api.github.com/user/codespaces/:name/exports/latest" + }`) + }) + + ctx := t.Context() + export, _, err := client.Codespaces.ExportCodespace(ctx, "codespace_1") + if err != nil { + t.Fatalf("Codespaces.ExportCodespace returned error: %v", err) + } + + want := &CodespaceExport{ + State: Ptr("succeeded"), + CompletedAt: Ptr(Timestamp{Time: time.Date(2025, time.December, 11, 0, 0, 0, 0, time.UTC)}), + Branch: Ptr("main"), + ExportURL: Ptr("https://api.github.com/user/codespaces/:name/exports/latest"), + } + + if !cmp.Equal(export, want) { + t.Errorf("Codespaces.ExportCodespace returned %+v, want %+v", export, want) + } + + const methodName = "ExportCodespace" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ExportCodespace(ctx, "codespace_1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_GetLatestCodespaceExport(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1/exports/latest", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "state": "succeeded", + "completed_at": "2025-12-11T00:00:00Z", + "branch": "main", + "export_url": "https://api.github.com/user/codespaces/:name/exports/latest" + }`) + }) + + ctx := t.Context() + export, _, err := client.Codespaces.GetLatestCodespaceExport(ctx, "codespace_1") + if err != nil { + t.Fatalf("Codespaces.GetLatestCodespaceExport returned error: %v", err) + } + + want := &CodespaceExport{ + State: Ptr("succeeded"), + CompletedAt: Ptr(Timestamp{Time: time.Date(2025, time.December, 11, 0, 0, 0, 0, time.UTC)}), + Branch: Ptr("main"), + ExportURL: Ptr("https://api.github.com/user/codespaces/:name/exports/latest"), + } + + if !cmp.Equal(export, want) { + t.Errorf("Codespaces.GetLatestCodespaceExport returned %+v, want %+v", export, want) + } + + const methodName = "GetLatestCodespaceExport" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.GetLatestCodespaceExport(ctx, "codespace_1") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_Publish(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1/publish", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody( + t, + r, + `{"name":"repo","private":true}`+"\n", + ) + fmt.Fprint(w, `{"id":1,"repository":{"id":111}}`) + }) + + opt := &PublishCodespaceOptions{ + Name: Ptr("repo"), + Private: Ptr(true), + } + + ctx := t.Context() + repo, _, err := client.Codespaces.Publish( + ctx, + "codespace_1", + opt, + ) + if err != nil { + t.Fatalf("Codespaces.Publish returned error: %v", err) + } + + want := &Codespace{ + ID: Ptr(int64(1)), + Repository: &Repository{ + ID: Ptr(int64(111)), + }, + } + if !cmp.Equal(repo, want) { + t.Errorf("Codespaces.Publish returned %+v, want %+v", repo, want) + } + + const methodName = "Publish" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.Publish( + ctx, + "codespace_1", + opt, + ) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index 6b7ec07c7e9..43d0e2e3d5c 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -3734,6 +3734,198 @@ func (c *Codespace) GetWebURL() string { return *c.WebURL } +// GetClientIP returns the ClientIP field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetClientIP() string { + if c == nil || c.ClientIP == nil { + return "" + } + return *c.ClientIP +} + +// GetDevcontainerPath returns the DevcontainerPath field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetDevcontainerPath() string { + if c == nil || c.DevcontainerPath == nil { + return "" + } + return *c.DevcontainerPath +} + +// GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetDisplayName() string { + if c == nil || c.DisplayName == nil { + return "" + } + return *c.DisplayName +} + +// GetGeo returns the Geo field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetGeo() string { + if c == nil || c.Geo == nil { + return "" + } + return *c.Geo +} + +// GetIdleTimeoutMinutes returns the IdleTimeoutMinutes field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetIdleTimeoutMinutes() int { + if c == nil || c.IdleTimeoutMinutes == nil { + return 0 + } + return *c.IdleTimeoutMinutes +} + +// GetLocation returns the Location field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetLocation() string { + if c == nil || c.Location == nil { + return "" + } + return *c.Location +} + +// GetMachine returns the Machine field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetMachine() string { + if c == nil || c.Machine == nil { + return "" + } + return *c.Machine +} + +// GetMultiRepoPermissionsOptOut returns the MultiRepoPermissionsOptOut field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetMultiRepoPermissionsOptOut() bool { + if c == nil || c.MultiRepoPermissionsOptOut == nil { + return false + } + return *c.MultiRepoPermissionsOptOut +} + +// GetPullRequest returns the PullRequest field. +func (c *CodespaceCreateForUserOptions) GetPullRequest() *CodespacePullRequestOptions { + if c == nil { + return nil + } + return c.PullRequest +} + +// GetRef returns the Ref field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetRef() string { + if c == nil || c.Ref == nil { + return "" + } + return *c.Ref +} + +// GetRetentionPeriodMinutes returns the RetentionPeriodMinutes field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetRetentionPeriodMinutes() int { + if c == nil || c.RetentionPeriodMinutes == nil { + return 0 + } + return *c.RetentionPeriodMinutes +} + +// GetWorkingDirectory returns the WorkingDirectory field if it's non-nil, zero value otherwise. +func (c *CodespaceCreateForUserOptions) GetWorkingDirectory() string { + if c == nil || c.WorkingDirectory == nil { + return "" + } + return *c.WorkingDirectory +} + +// GetBillableOwner returns the BillableOwner field. +func (c *CodespaceDefaultAttributes) GetBillableOwner() *User { + if c == nil { + return nil + } + return c.BillableOwner +} + +// GetDefaults returns the Defaults field. +func (c *CodespaceDefaultAttributes) GetDefaults() *CodespaceDefaults { + if c == nil { + return nil + } + return c.Defaults +} + +// GetDevcontainerPath returns the DevcontainerPath field if it's non-nil, zero value otherwise. +func (c *CodespaceDefaults) GetDevcontainerPath() string { + if c == nil || c.DevcontainerPath == nil { + return "" + } + return *c.DevcontainerPath +} + +// GetBranch returns the Branch field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetBranch() string { + if c == nil || c.Branch == nil { + return "" + } + return *c.Branch +} + +// GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetCompletedAt() Timestamp { + if c == nil || c.CompletedAt == nil { + return Timestamp{} + } + return *c.CompletedAt +} + +// GetExportURL returns the ExportURL field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetExportURL() string { + if c == nil || c.ExportURL == nil { + return "" + } + return *c.ExportURL +} + +// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetHTMLURL() string { + if c == nil || c.HTMLURL == nil { + return "" + } + return *c.HTMLURL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetID() string { + if c == nil || c.ID == nil { + return "" + } + return *c.ID +} + +// GetSHA returns the SHA field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetSHA() string { + if c == nil || c.SHA == nil { + return "" + } + return *c.SHA +} + +// GetState returns the State field if it's non-nil, zero value otherwise. +func (c *CodespaceExport) GetState() string { + if c == nil || c.State == nil { + return "" + } + return *c.State +} + +// GetClientIP returns the ClientIP field if it's non-nil, zero value otherwise. +func (c *CodespaceGetDefaultAttributesOptions) GetClientIP() string { + if c == nil || c.ClientIP == nil { + return "" + } + return *c.ClientIP +} + +// GetRef returns the Ref field if it's non-nil, zero value otherwise. +func (c *CodespaceGetDefaultAttributesOptions) GetRef() string { + if c == nil || c.Ref == nil { + return "" + } + return *c.Ref +} + // GetAhead returns the Ahead field if it's non-nil, zero value otherwise. func (c *CodespacesGitStatus) GetAhead() int { if c == nil || c.Ahead == nil { @@ -6262,6 +6454,14 @@ func (c *CreateCodespaceOptions) GetIdleTimeoutMinutes() int { return *c.IdleTimeoutMinutes } +// GetLocation returns the Location field if it's non-nil, zero value otherwise. +func (c *CreateCodespaceOptions) GetLocation() string { + if c == nil || c.Location == nil { + return "" + } + return *c.Location +} + // GetMachine returns the Machine field if it's non-nil, zero value otherwise. func (c *CreateCodespaceOptions) GetMachine() string { if c == nil || c.Machine == nil { @@ -8462,6 +8662,22 @@ func (d *DeploymentStatusRequest) GetState() string { return *d.State } +// GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise. +func (d *DevContainer) GetDisplayName() string { + if d == nil || d.DisplayName == nil { + return "" + } + return *d.DisplayName +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (d *DevContainer) GetName() string { + if d == nil || d.Name == nil { + return "" + } + return *d.Name +} + // GetActiveLockReason returns the ActiveLockReason field if it's non-nil, zero value otherwise. func (d *Discussion) GetActiveLockReason() string { if d == nil || d.ActiveLockReason == nil { @@ -20174,6 +20390,22 @@ func (p *PublicKey) GetKeyID() string { return *p.KeyID } +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *PublishCodespaceOptions) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetPrivate returns the Private field if it's non-nil, zero value otherwise. +func (p *PublishCodespaceOptions) GetPrivate() bool { + if p == nil || p.Private == nil { + return false + } + return *p.Private +} + // GetActiveLockReason returns the ActiveLockReason field if it's non-nil, zero value otherwise. func (p *PullRequest) GetActiveLockReason() string { if p == nil || p.ActiveLockReason == nil { @@ -29638,6 +29870,14 @@ func (u *UpdateCheckRunOptions) GetStatus() string { return *u.Status } +// GetMachine returns the Machine field if it's non-nil, zero value otherwise. +func (u *UpdateCodespaceOptions) GetMachine() string { + if u == nil || u.Machine == nil { + return "" + } + return *u.Machine +} + // GetQuerySuite returns the QuerySuite field if it's non-nil, zero value otherwise. func (u *UpdateDefaultSetupConfigurationOptions) GetQuerySuite() string { if u == nil || u.QuerySuite == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 766bc5f2593..78249314e47 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -4872,6 +4872,261 @@ func TestCodespace_GetWebURL(tt *testing.T) { c.GetWebURL() } +func TestCodespaceCreateForUserOptions_GetClientIP(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{ClientIP: &zeroValue} + c.GetClientIP() + c = &CodespaceCreateForUserOptions{} + c.GetClientIP() + c = nil + c.GetClientIP() +} + +func TestCodespaceCreateForUserOptions_GetDevcontainerPath(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{DevcontainerPath: &zeroValue} + c.GetDevcontainerPath() + c = &CodespaceCreateForUserOptions{} + c.GetDevcontainerPath() + c = nil + c.GetDevcontainerPath() +} + +func TestCodespaceCreateForUserOptions_GetDisplayName(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{DisplayName: &zeroValue} + c.GetDisplayName() + c = &CodespaceCreateForUserOptions{} + c.GetDisplayName() + c = nil + c.GetDisplayName() +} + +func TestCodespaceCreateForUserOptions_GetGeo(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{Geo: &zeroValue} + c.GetGeo() + c = &CodespaceCreateForUserOptions{} + c.GetGeo() + c = nil + c.GetGeo() +} + +func TestCodespaceCreateForUserOptions_GetIdleTimeoutMinutes(tt *testing.T) { + tt.Parallel() + var zeroValue int + c := &CodespaceCreateForUserOptions{IdleTimeoutMinutes: &zeroValue} + c.GetIdleTimeoutMinutes() + c = &CodespaceCreateForUserOptions{} + c.GetIdleTimeoutMinutes() + c = nil + c.GetIdleTimeoutMinutes() +} + +func TestCodespaceCreateForUserOptions_GetLocation(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{Location: &zeroValue} + c.GetLocation() + c = &CodespaceCreateForUserOptions{} + c.GetLocation() + c = nil + c.GetLocation() +} + +func TestCodespaceCreateForUserOptions_GetMachine(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{Machine: &zeroValue} + c.GetMachine() + c = &CodespaceCreateForUserOptions{} + c.GetMachine() + c = nil + c.GetMachine() +} + +func TestCodespaceCreateForUserOptions_GetMultiRepoPermissionsOptOut(tt *testing.T) { + tt.Parallel() + var zeroValue bool + c := &CodespaceCreateForUserOptions{MultiRepoPermissionsOptOut: &zeroValue} + c.GetMultiRepoPermissionsOptOut() + c = &CodespaceCreateForUserOptions{} + c.GetMultiRepoPermissionsOptOut() + c = nil + c.GetMultiRepoPermissionsOptOut() +} + +func TestCodespaceCreateForUserOptions_GetPullRequest(tt *testing.T) { + tt.Parallel() + c := &CodespaceCreateForUserOptions{} + c.GetPullRequest() + c = nil + c.GetPullRequest() +} + +func TestCodespaceCreateForUserOptions_GetRef(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{Ref: &zeroValue} + c.GetRef() + c = &CodespaceCreateForUserOptions{} + c.GetRef() + c = nil + c.GetRef() +} + +func TestCodespaceCreateForUserOptions_GetRetentionPeriodMinutes(tt *testing.T) { + tt.Parallel() + var zeroValue int + c := &CodespaceCreateForUserOptions{RetentionPeriodMinutes: &zeroValue} + c.GetRetentionPeriodMinutes() + c = &CodespaceCreateForUserOptions{} + c.GetRetentionPeriodMinutes() + c = nil + c.GetRetentionPeriodMinutes() +} + +func TestCodespaceCreateForUserOptions_GetWorkingDirectory(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceCreateForUserOptions{WorkingDirectory: &zeroValue} + c.GetWorkingDirectory() + c = &CodespaceCreateForUserOptions{} + c.GetWorkingDirectory() + c = nil + c.GetWorkingDirectory() +} + +func TestCodespaceDefaultAttributes_GetBillableOwner(tt *testing.T) { + tt.Parallel() + c := &CodespaceDefaultAttributes{} + c.GetBillableOwner() + c = nil + c.GetBillableOwner() +} + +func TestCodespaceDefaultAttributes_GetDefaults(tt *testing.T) { + tt.Parallel() + c := &CodespaceDefaultAttributes{} + c.GetDefaults() + c = nil + c.GetDefaults() +} + +func TestCodespaceDefaults_GetDevcontainerPath(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceDefaults{DevcontainerPath: &zeroValue} + c.GetDevcontainerPath() + c = &CodespaceDefaults{} + c.GetDevcontainerPath() + c = nil + c.GetDevcontainerPath() +} + +func TestCodespaceExport_GetBranch(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{Branch: &zeroValue} + c.GetBranch() + c = &CodespaceExport{} + c.GetBranch() + c = nil + c.GetBranch() +} + +func TestCodespaceExport_GetCompletedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + c := &CodespaceExport{CompletedAt: &zeroValue} + c.GetCompletedAt() + c = &CodespaceExport{} + c.GetCompletedAt() + c = nil + c.GetCompletedAt() +} + +func TestCodespaceExport_GetExportURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{ExportURL: &zeroValue} + c.GetExportURL() + c = &CodespaceExport{} + c.GetExportURL() + c = nil + c.GetExportURL() +} + +func TestCodespaceExport_GetHTMLURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{HTMLURL: &zeroValue} + c.GetHTMLURL() + c = &CodespaceExport{} + c.GetHTMLURL() + c = nil + c.GetHTMLURL() +} + +func TestCodespaceExport_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{ID: &zeroValue} + c.GetID() + c = &CodespaceExport{} + c.GetID() + c = nil + c.GetID() +} + +func TestCodespaceExport_GetSHA(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{SHA: &zeroValue} + c.GetSHA() + c = &CodespaceExport{} + c.GetSHA() + c = nil + c.GetSHA() +} + +func TestCodespaceExport_GetState(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceExport{State: &zeroValue} + c.GetState() + c = &CodespaceExport{} + c.GetState() + c = nil + c.GetState() +} + +func TestCodespaceGetDefaultAttributesOptions_GetClientIP(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceGetDefaultAttributesOptions{ClientIP: &zeroValue} + c.GetClientIP() + c = &CodespaceGetDefaultAttributesOptions{} + c.GetClientIP() + c = nil + c.GetClientIP() +} + +func TestCodespaceGetDefaultAttributesOptions_GetRef(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CodespaceGetDefaultAttributesOptions{Ref: &zeroValue} + c.GetRef() + c = &CodespaceGetDefaultAttributesOptions{} + c.GetRef() + c = nil + c.GetRef() +} + func TestCodespacesGitStatus_GetAhead(tt *testing.T) { tt.Parallel() var zeroValue int @@ -8174,6 +8429,17 @@ func TestCreateCodespaceOptions_GetIdleTimeoutMinutes(tt *testing.T) { c.GetIdleTimeoutMinutes() } +func TestCreateCodespaceOptions_GetLocation(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateCodespaceOptions{Location: &zeroValue} + c.GetLocation() + c = &CreateCodespaceOptions{} + c.GetLocation() + c = nil + c.GetLocation() +} + func TestCreateCodespaceOptions_GetMachine(tt *testing.T) { tt.Parallel() var zeroValue string @@ -10974,6 +11240,28 @@ func TestDeploymentStatusRequest_GetState(tt *testing.T) { d.GetState() } +func TestDevContainer_GetDisplayName(tt *testing.T) { + tt.Parallel() + var zeroValue string + d := &DevContainer{DisplayName: &zeroValue} + d.GetDisplayName() + d = &DevContainer{} + d.GetDisplayName() + d = nil + d.GetDisplayName() +} + +func TestDevContainer_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + d := &DevContainer{Name: &zeroValue} + d.GetName() + d = &DevContainer{} + d.GetName() + d = nil + d.GetName() +} + func TestDiscussion_GetActiveLockReason(tt *testing.T) { tt.Parallel() var zeroValue string @@ -26130,6 +26418,28 @@ func TestPublicKey_GetKeyID(tt *testing.T) { p.GetKeyID() } +func TestPublishCodespaceOptions_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &PublishCodespaceOptions{Name: &zeroValue} + p.GetName() + p = &PublishCodespaceOptions{} + p.GetName() + p = nil + p.GetName() +} + +func TestPublishCodespaceOptions_GetPrivate(tt *testing.T) { + tt.Parallel() + var zeroValue bool + p := &PublishCodespaceOptions{Private: &zeroValue} + p.GetPrivate() + p = &PublishCodespaceOptions{} + p.GetPrivate() + p = nil + p.GetPrivate() +} + func TestPullRequest_GetActiveLockReason(tt *testing.T) { tt.Parallel() var zeroValue string @@ -38189,6 +38499,17 @@ func TestUpdateCheckRunOptions_GetStatus(tt *testing.T) { u.GetStatus() } +func TestUpdateCodespaceOptions_GetMachine(tt *testing.T) { + tt.Parallel() + var zeroValue string + u := &UpdateCodespaceOptions{Machine: &zeroValue} + u.GetMachine() + u = &UpdateCodespaceOptions{} + u.GetMachine() + u = nil + u.GetMachine() +} + func TestUpdateDefaultSetupConfigurationOptions_GetQuerySuite(tt *testing.T) { tt.Parallel() var zeroValue string From c98fd62b4c01ca22bcb67572b73f5eac258c2bc4 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Sat, 20 Dec 2025 01:37:00 +0530 Subject: [PATCH 43/49] feat: Handle `omitzero` in `structfield` linter (#3881) --- tools/structfield/structfield.go | 93 +++++++++++++++++-- .../testdata/src/has-warnings/main.go | 20 ++++ .../testdata/src/no-warnings/main.go | 10 ++ 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/tools/structfield/structfield.go b/tools/structfield/structfield.go index 19ac0db4de8..dd25e538877 100644 --- a/tools/structfield/structfield.go +++ b/tools/structfield/structfield.go @@ -146,18 +146,83 @@ func processTag(structName string, goField *ast.Ident, field *ast.Field, structT return } - if strings.Contains(tagName, ",omitempty") { - checkGoFieldType(structName, goField.Name, field, field.Type.Pos(), pass, allowedTagTypes) + hasOmitEmpty := strings.Contains(tagName, ",omitempty") + hasOmitZero := strings.Contains(tagName, ",omitzero") + + if hasOmitEmpty || hasOmitZero { + if tagType == "url" && hasOmitZero { + const msg = "the %q field in struct %q uses unsupported omitzero tag for URL tags" + pass.Reportf(field.Pos(), msg, goField.Name, structName) + } else { + checkGoFieldType(structName, goField.Name, field, field.Pos(), pass, allowedTagTypes, hasOmitEmpty, hasOmitZero) + } + tagName = strings.ReplaceAll(tagName, ",omitzero", "") tagName = strings.ReplaceAll(tagName, ",omitempty", "") } - if tagType == "url" { tagName = strings.ReplaceAll(tagName, ",comma", "") } - checkGoFieldName(structName, goField.Name, tagName, goField.Pos(), pass, allowedTagNames) } +func checkAndReportInvalidTypesForOmitzero(structName, goFieldName string, fieldType ast.Expr, tokenPos token.Pos, pass *analysis.Pass) bool { + switch ft := fieldType.(type) { + case *ast.StarExpr: + // Check for *[]T where T is builtin - should be []T + if arrType, ok := ft.X.(*ast.ArrayType); ok { + if ident, ok := arrType.Elt.(*ast.Ident); ok && isBuiltinType(ident.Name) { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]"+ident.Name, structName) + } else if starExpr, ok := arrType.Elt.(*ast.StarExpr); ok { + // Check for *[]*T - should be []*T + if ident, ok := starExpr.X.(*ast.Ident); ok { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "[]*"+ident.Name, structName) + } + } else { + checkStructArrayType(structName, goFieldName, arrType, tokenPos, pass) + } + return true + } + // Check for *int - should not to be used with omitzero only with omitempty + if ident, ok := ft.X.(*ast.Ident); ok { + if isBuiltinType(ident.Name) { + const msg = `the %q field in struct %q uses "omitzero" with a primitive type; remove "omitzero" and use only "omitempty" for pointer primitive types"` + pass.Reportf(tokenPos, msg, goFieldName, structName) + return true + } + } + // Check for *map - should be map + if _, ok := ft.X.(*ast.MapType); ok { + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, exprToString(ft.X), structName) + return true + } + return true + case *ast.MapType: + return true + case *ast.ArrayType: + checkStructArrayType(structName, goFieldName, ft, tokenPos, pass) + return true + case *ast.Ident: + if obj := pass.TypesInfo.ObjectOf(ft); obj != nil { + switch obj.Type().Underlying().(type) { + case *types.Struct: + // For Struct - should be *Struct + const msg = "change the %q field type to %q in the struct %q" + pass.Reportf(tokenPos, msg, goFieldName, "*"+ft.Name, structName) + return true + case *types.Basic: + // For Builtin - should not to be used with omitzero + const msg = `the %q field in struct %q uses "omitzero" with a primitive type; remove "omitzero", as it is only allowed with structs, maps, and slices` + pass.Reportf(tokenPos, msg, goFieldName, structName) + return true + } + } + } + return false +} + func checkGoFieldName(structName, goFieldName, tagName string, tokenPos token.Pos, pass *analysis.Pass, allowedNames map[string]bool) { fullName := structName + "." + goFieldName if allowedNames[fullName] { @@ -171,16 +236,24 @@ func checkGoFieldName(structName, goFieldName, tagName string, tokenPos token.Po } } -func checkGoFieldType(structName, goFieldName string, field *ast.Field, tokenPos token.Pos, pass *analysis.Pass, allowedTypes map[string]bool) { +func checkGoFieldType(structName, goFieldName string, field *ast.Field, tokenPos token.Pos, pass *analysis.Pass, allowedTypes map[string]bool, omitempty, omitzero bool) { if allowedTypes[structName+"."+goFieldName] { return } + switch { + case omitzero: + skipOmitzero := checkAndReportInvalidTypesForOmitzero(structName, goFieldName, field.Type, tokenPos, pass) + if !skipOmitzero { + const msg = `the %q field in struct %q uses "omitzero"; remove "omitzero", as it is only allowed with structs, maps, and slices` + pass.Reportf(tokenPos, msg, goFieldName, structName) + } - skipOmitempty := checkAndReportInvalidTypes(structName, goFieldName, field.Type, tokenPos, pass) - - if !skipOmitempty { - const msg = `change the %q field type to %q in the struct %q because its tag uses "omitempty"` - pass.Reportf(tokenPos, msg, goFieldName, "*"+exprToString(field.Type), structName) + case omitempty: + skipOmitempty := checkAndReportInvalidTypes(structName, goFieldName, field.Type, tokenPos, pass) + if !skipOmitempty { + const msg = `change the %q field type to %q in the struct %q because its tag uses "omitempty"` + pass.Reportf(tokenPos, msg, goFieldName, "*"+exprToString(field.Type), structName) + } } } diff --git a/tools/structfield/testdata/src/has-warnings/main.go b/tools/structfield/testdata/src/has-warnings/main.go index 26d2704fc78..4d0576e1294 100644 --- a/tools/structfield/testdata/src/has-warnings/main.go +++ b/tools/structfield/testdata/src/has-warnings/main.go @@ -22,6 +22,23 @@ type JSONFieldType struct { PointerToSliceOfPointerStructs *[]*Struct `json:"pointer_to_slice_of_pointer_structs,omitempty"` // want `change the "PointerToSliceOfPointerStructs" field type to "\[\]\*Struct" in the struct "JSONFieldType"` PointerToMap *map[string]string `json:"pointer_to_map,omitempty"` // want `change the "PointerToMap" field type to "map\[string\]string" in the struct "JSONFieldType"` SliceOfInts []*int `json:"slice_of_ints,omitempty"` // want `change the "SliceOfInts" field type to "\[\]int" in the struct "JSONFieldType"` + + Count int `json:"count,omitzero"` // want `the "Count" field in struct "JSONFieldType" uses "omitzero" with a primitive type; remove "omitzero", as it is only allowed with structs, maps, and slices` + Size *int `json:"size,omitzero"` // want `the "Size" field in struct "JSONFieldType" uses "omitzero" with a primitive type; remove "omitzero" and use only "omitempty" for pointer primitive types` + PointerToSliceOfStringsZero *[]string `json:"pointer_to_slice_of_strings_zero,omitzero"` // want `change the "PointerToSliceOfStringsZero" field type to "\[\]string" in the struct "JSONFieldType"` + PointerToSliceOfStructsZero *[]Struct `json:"pointer_to_slice_of_structs_zero,omitzero"` // want `change the "PointerToSliceOfStructsZero" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + PointerToSliceOfPointerStructsZero *[]*Struct `json:"pointer_to_slice_of_pointer_structs_zero,omitzero"` // want `change the "PointerToSliceOfPointerStructsZero" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + PointerSliceInt *[]int `json:"pointer_slice_int,omitzero"` // want `change the "PointerSliceInt" field type to "\[\]int" in the struct "JSONFieldType"` + AnyZero any `json:"any_zero,omitzero"` // want `the "AnyZero" field in struct "JSONFieldType" uses "omitzero"; remove "omitzero", as it is only allowed with structs, maps, and slices` + StringPointerSlice []*string `json:"string_pointer_slice,omitzero"` // want `change the "StringPointerSlice" field type to "\[\]string" in the struct "JSONFieldType"` + PointerToMapZero *map[string]string `json:"pointer__to_map_zero,omitzero"` // want `change the "PointerToMapZero" field type to "map\[string\]string" in the struct "JSONFieldType"` + SliceOfStructsZero []Struct `json:"slice_of_structs_zero,omitzero"` // want `change the "SliceOfStructsZero" field type to "\[\]\*Struct" in the struct "JSONFieldType"` + StructZero Struct `json:"struct_zero,omitzero"` // want `change the "StructZero" field type to "\*Struct" in the struct "JSONFieldType"` + + AnyBoth any `json:"any_both,omitempty,omitzero"` // want `the "AnyBoth" field in struct "JSONFieldType" uses "omitzero"; remove "omitzero", as it is only allowed with structs, maps, and slices` + NonPointerStructBoth Struct `json:"non_pointer_struct_both,omitempty,omitzero"` // want `change the "NonPointerStructBoth" field type to "\*Struct" in the struct "JSONFieldType"` + PointerStringBoth *string `json:"pointer_string_both,omitempty,omitzero"` // want `the "PointerStringBoth" field in struct "JSONFieldType" uses "omitzero" with a primitive type; remove "omitzero" and use only "omitempty" for pointer primitive types` + StructZeroBoth Struct `json:"struct_zero_both,omitempty,omitzero"` // want `change the "StructZeroBoth" field type to "\*Struct" in the struct "JSONFieldType"` } type Struct struct{} @@ -34,4 +51,7 @@ type URLFieldType struct { Page string `url:"page,omitempty"` // want `change the "Page" field type to "\*string" in the struct "URLFieldType" because its tag uses "omitempty"` PerPage int `url:"per_page,omitempty"` // want `change the "PerPage" field type to "\*int" in the struct "URLFieldType" because its tag uses "omitempty"` Participating bool `url:"participating,omitempty"` // want `change the "Participating" field type to "\*bool" in the struct "URLFieldType" because its tag uses "omitempty"` + + PerPageZeros []int `url:"per_page_zeros,omitzero"` // want `the "PerPageZeros" field in struct "URLFieldType" uses unsupported omitzero tag for URL tags` + PerPageBoth *int `url:"per_page_both,omitempty,omitzero"` // want `the "PerPageBoth" field in struct "URLFieldType" uses unsupported omitzero tag for URL tags` } diff --git a/tools/structfield/testdata/src/no-warnings/main.go b/tools/structfield/testdata/src/no-warnings/main.go index fc0236f1cfe..7c43789c659 100644 --- a/tools/structfield/testdata/src/no-warnings/main.go +++ b/tools/structfield/testdata/src/no-warnings/main.go @@ -27,6 +27,16 @@ type JSONFieldType struct { Exception string `json:"exception,omitempty"` Value any `json:"value,omitempty"` SliceOfPointerStructs []*Struct `json:"slice_of_pointer_structs,omitempty"` + + SliceOfStrings []string `json:"slice_of_strings,omitzero"` + MapOfStringToInt map[string]int `json:"map_of_string_to_int,omitzero"` + PointerStructField *Struct `json:"pointer_struct_field,omitzero"` + SliceOfPointerStructsZero []*Struct `json:"slice_of_pointer_structs_zero,omitzero"` + + SliceOfStringsBoth []string `json:"slice_of_strings_both,omitzero,omitempty"` + MapOfStringToIntBoth map[string]int `json:"map_of_string_to_int_both,omitzero,omitempty"` + SliceOfPointerStructsBoth []*Struct `json:"slice_of_pointer_structs_both,omitzero,omitempty"` + StructFieldBoth *Struct `json:"struct_field_both,omitzero,omitempty"` } type URLFieldName struct { From e10040dccdf7585f76d5a453a988d7b901e8a98e Mon Sep 17 00:00:00 2001 From: Tom Elliott <13594679+tmelliottjr@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:19:25 -0500 Subject: [PATCH 44/49] fix!: Add support for missing attributes in ProjectsV2 API (#3888) --- github/event_types.go | 38 ---- github/event_types_test.go | 2 +- github/github-accessors.go | 226 ++++++++++++++++++- github/github-accessors_test.go | 278 +++++++++++++++++++++++- github/github-stringify_test.go | 6 +- github/projects.go | 176 +++++++++++++-- github/projects_test.go | 372 +++++++++++++++++++++++++++++++- 7 files changed, 1027 insertions(+), 71 deletions(-) diff --git a/github/event_types.go b/github/event_types.go index eb1fd57a9a3..2a085d47b60 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1148,44 +1148,6 @@ type FieldValue struct { To json.RawMessage `json:"to,omitempty"` } -// ProjectV2ItemFieldValue represents a field value of a project item. -type ProjectV2ItemFieldValue struct { - ID *int64 `json:"id,omitempty"` - Name string `json:"name,omitempty"` - DataType string `json:"data_type,omitempty"` - // Value set for the field. The type depends on the field type: - // - text: string - // - number: float64 - // - date: string (ISO 8601 date format, e.g. "2023-06-23") or null - // - single_select: object with "id", "name", "color", "description" fields or null - // - iteration: object with "id", "title", "start_date", "duration" fields or null - // - title: object with "text" field (read-only, reflects the item's title) or null - // - assignees: array of user objects with "login", "id", etc. or null - // - labels: array of label objects with "id", "name", "color", etc. or null - // - linked_pull_requests: array of pull request objects or null - // - milestone: milestone object with "id", "title", "description", etc. or null - // - repository: repository object with "id", "name", "full_name", etc. or null - // - reviewers: array of user objects or null - // - status: object with "id", "name", "color", "description" fields (same structure as single_select) or null - Value any `json:"value,omitempty"` -} - -// ProjectV2Item represents an item belonging to a project. -type ProjectV2Item struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *User `json:"creator,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - ArchivedAt *Timestamp `json:"archived_at,omitempty"` - ItemURL *string `json:"item_url,omitempty"` - Fields []*ProjectV2ItemFieldValue `json:"fields,omitempty"` -} - // PublicEvent is triggered when a private repository is open sourced. // According to GitHub: "Without a doubt: the best GitHub event." // The Webhook event name is "public". diff --git a/github/event_types_test.go b/github/event_types_test.go index 15881ab6cec..955de1b09e0 100644 --- a/github/event_types_test.go +++ b/github/event_types_test.go @@ -15866,7 +15866,7 @@ func TestProjectV2ItemEvent_Marshal(t *testing.T) { NodeID: Ptr("nid"), ProjectNodeID: Ptr("pnid"), ContentNodeID: Ptr("cnid"), - ContentType: Ptr("ct"), + ContentType: Ptr(ProjectV2ItemContentType("ct")), Creator: &User{ Login: Ptr("l"), ID: Ptr(int64(1)), diff --git a/github/github-accessors.go b/github/github-accessors.go index 43d0e2e3d5c..b907c2af56a 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -358,6 +358,22 @@ func (a *ActorLocation) GetCountryCode() string { return *a.CountryCode } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (a *AddProjectItemOptions) GetID() int64 { + if a == nil || a.ID == nil { + return 0 + } + return *a.ID +} + +// GetType returns the Type field. +func (a *AddProjectItemOptions) GetType() *ProjectV2ItemContentType { + if a == nil { + return nil + } + return a.Type +} + // GetMessage returns the Message field if it's non-nil, zero value otherwise. func (a *AddResourcesToCostCenterResponse) GetMessage() string { if a == nil || a.Message == nil { @@ -19526,6 +19542,22 @@ func (p *ProjectV2) GetID() int64 { return *p.ID } +// GetIsTemplate returns the IsTemplate field if it's non-nil, zero value otherwise. +func (p *ProjectV2) GetIsTemplate() bool { + if p == nil || p.IsTemplate == nil { + return false + } + return *p.IsTemplate +} + +// GetLatestStatusUpdate returns the LatestStatusUpdate field. +func (p *ProjectV2) GetLatestStatusUpdate() *ProjectV2StatusUpdate { + if p == nil { + return nil + } + return p.LatestStatusUpdate +} + // GetName returns the Name field if it's non-nil, zero value otherwise. func (p *ProjectV2) GetName() string { if p == nil || p.Name == nil { @@ -19630,6 +19662,62 @@ func (p *ProjectV2) GetURL() string { return *p.URL } +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetBody() string { + if p == nil || p.Body == nil { + return "" + } + return *p.Body +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetTitle() string { + if p == nil || p.Title == nil { + return "" + } + return *p.Title +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2DraftIssue) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + +// GetUser returns the User field. +func (p *ProjectV2DraftIssue) GetUser() *User { + if p == nil { + return nil + } + return p.User +} + // GetAction returns the Action field if it's non-nil, zero value otherwise. func (p *ProjectV2Event) GetAction() string { if p == nil || p.Action == nil { @@ -19822,6 +19910,14 @@ func (p *ProjectV2Item) GetArchivedAt() Timestamp { return *p.ArchivedAt } +// GetContent returns the Content field. +func (p *ProjectV2Item) GetContent() *ProjectV2ItemContent { + if p == nil { + return nil + } + return p.Content +} + // GetContentNodeID returns the ContentNodeID field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetContentNodeID() string { if p == nil || p.ContentNodeID == nil { @@ -19830,12 +19926,12 @@ func (p *ProjectV2Item) GetContentNodeID() string { return *p.ContentNodeID } -// GetContentType returns the ContentType field if it's non-nil, zero value otherwise. -func (p *ProjectV2Item) GetContentType() string { - if p == nil || p.ContentType == nil { - return "" +// GetContentType returns the ContentType field. +func (p *ProjectV2Item) GetContentType() *ProjectV2ItemContentType { + if p == nil { + return nil } - return *p.ContentType + return p.ContentType } // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. @@ -19918,6 +20014,30 @@ func (p *ProjectV2ItemChange) GetFieldValue() *FieldValue { return p.FieldValue } +// GetDraftIssue returns the DraftIssue field. +func (p *ProjectV2ItemContent) GetDraftIssue() *ProjectV2DraftIssue { + if p == nil { + return nil + } + return p.DraftIssue +} + +// GetIssue returns the Issue field. +func (p *ProjectV2ItemContent) GetIssue() *Issue { + if p == nil { + return nil + } + return p.Issue +} + +// GetPullRequest returns the PullRequest field. +func (p *ProjectV2ItemContent) GetPullRequest() *PullRequest { + if p == nil { + return nil + } + return p.PullRequest +} + // GetAction returns the Action field if it's non-nil, zero value otherwise. func (p *ProjectV2ItemEvent) GetAction() string { if p == nil || p.Action == nil { @@ -19966,6 +20086,14 @@ func (p *ProjectV2ItemEvent) GetSender() *User { return p.Sender } +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (p *ProjectV2ItemFieldValue) GetDataType() string { + if p == nil || p.DataType == nil { + return "" + } + return *p.DataType +} + // GetID returns the ID field if it's non-nil, zero value otherwise. func (p *ProjectV2ItemFieldValue) GetID() int64 { if p == nil || p.ID == nil { @@ -19974,6 +20102,94 @@ func (p *ProjectV2ItemFieldValue) GetID() int64 { return *p.ID } +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2ItemFieldValue) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetBody() string { + if p == nil || p.Body == nil { + return "" + } + return *p.Body +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetCreator returns the Creator field. +func (p *ProjectV2StatusUpdate) GetCreator() *User { + if p == nil { + return nil + } + return p.Creator +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetProjectNodeID returns the ProjectNodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetProjectNodeID() string { + if p == nil || p.ProjectNodeID == nil { + return "" + } + return *p.ProjectNodeID +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetStatus() string { + if p == nil || p.Status == nil { + return "" + } + return *p.Status +} + +// GetTargetDate returns the TargetDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetTargetDate() string { + if p == nil || p.TargetDate == nil { + return "" + } + return *p.TargetDate +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2StatusUpdate) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + // GetHTML returns the HTML field if it's non-nil, zero value otherwise. func (p *ProjectV2TextContent) GetHTML() string { if p == nil || p.HTML == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 78249314e47..b12c405d2bf 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -479,6 +479,25 @@ func TestActorLocation_GetCountryCode(tt *testing.T) { a.GetCountryCode() } +func TestAddProjectItemOptions_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + a := &AddProjectItemOptions{ID: &zeroValue} + a.GetID() + a = &AddProjectItemOptions{} + a.GetID() + a = nil + a.GetID() +} + +func TestAddProjectItemOptions_GetType(tt *testing.T) { + tt.Parallel() + a := &AddProjectItemOptions{} + a.GetType() + a = nil + a.GetType() +} + func TestAddResourcesToCostCenterResponse_GetMessage(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25386,6 +25405,25 @@ func TestProjectV2_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2_GetIsTemplate(tt *testing.T) { + tt.Parallel() + var zeroValue bool + p := &ProjectV2{IsTemplate: &zeroValue} + p.GetIsTemplate() + p = &ProjectV2{} + p.GetIsTemplate() + p = nil + p.GetIsTemplate() +} + +func TestProjectV2_GetLatestStatusUpdate(tt *testing.T) { + tt.Parallel() + p := &ProjectV2{} + p.GetLatestStatusUpdate() + p = nil + p.GetLatestStatusUpdate() +} + func TestProjectV2_GetName(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25526,6 +25564,80 @@ func TestProjectV2_GetURL(tt *testing.T) { p.GetURL() } +func TestProjectV2DraftIssue_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{Body: &zeroValue} + p.GetBody() + p = &ProjectV2DraftIssue{} + p.GetBody() + p = nil + p.GetBody() +} + +func TestProjectV2DraftIssue_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2DraftIssue{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2DraftIssue{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2DraftIssue_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2DraftIssue{ID: &zeroValue} + p.GetID() + p = &ProjectV2DraftIssue{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2DraftIssue_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2DraftIssue{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2DraftIssue_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2DraftIssue{Title: &zeroValue} + p.GetTitle() + p = &ProjectV2DraftIssue{} + p.GetTitle() + p = nil + p.GetTitle() +} + +func TestProjectV2DraftIssue_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2DraftIssue{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2DraftIssue{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + +func TestProjectV2DraftIssue_GetUser(tt *testing.T) { + tt.Parallel() + p := &ProjectV2DraftIssue{} + p.GetUser() + p = nil + p.GetUser() +} + func TestProjectV2Event_GetAction(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25766,6 +25878,14 @@ func TestProjectV2Item_GetArchivedAt(tt *testing.T) { p.GetArchivedAt() } +func TestProjectV2Item_GetContent(tt *testing.T) { + tt.Parallel() + p := &ProjectV2Item{} + p.GetContent() + p = nil + p.GetContent() +} + func TestProjectV2Item_GetContentNodeID(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25779,10 +25899,7 @@ func TestProjectV2Item_GetContentNodeID(tt *testing.T) { func TestProjectV2Item_GetContentType(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2Item{ContentType: &zeroValue} - p.GetContentType() - p = &ProjectV2Item{} + p := &ProjectV2Item{} p.GetContentType() p = nil p.GetContentType() @@ -25889,6 +26006,30 @@ func TestProjectV2ItemChange_GetFieldValue(tt *testing.T) { p.GetFieldValue() } +func TestProjectV2ItemContent_GetDraftIssue(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetDraftIssue() + p = nil + p.GetDraftIssue() +} + +func TestProjectV2ItemContent_GetIssue(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetIssue() + p = nil + p.GetIssue() +} + +func TestProjectV2ItemContent_GetPullRequest(tt *testing.T) { + tt.Parallel() + p := &ProjectV2ItemContent{} + p.GetPullRequest() + p = nil + p.GetPullRequest() +} + func TestProjectV2ItemEvent_GetAction(tt *testing.T) { tt.Parallel() var zeroValue string @@ -25940,6 +26081,17 @@ func TestProjectV2ItemEvent_GetSender(tt *testing.T) { p.GetSender() } +func TestProjectV2ItemFieldValue_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ItemFieldValue{DataType: &zeroValue} + p.GetDataType() + p = &ProjectV2ItemFieldValue{} + p.GetDataType() + p = nil + p.GetDataType() +} + func TestProjectV2ItemFieldValue_GetID(tt *testing.T) { tt.Parallel() var zeroValue int64 @@ -25951,6 +26103,124 @@ func TestProjectV2ItemFieldValue_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2ItemFieldValue_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ItemFieldValue{Name: &zeroValue} + p.GetName() + p = &ProjectV2ItemFieldValue{} + p.GetName() + p = nil + p.GetName() +} + +func TestProjectV2StatusUpdate_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{Body: &zeroValue} + p.GetBody() + p = &ProjectV2StatusUpdate{} + p.GetBody() + p = nil + p.GetBody() +} + +func TestProjectV2StatusUpdate_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2StatusUpdate{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2StatusUpdate{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2StatusUpdate_GetCreator(tt *testing.T) { + tt.Parallel() + p := &ProjectV2StatusUpdate{} + p.GetCreator() + p = nil + p.GetCreator() +} + +func TestProjectV2StatusUpdate_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2StatusUpdate{ID: &zeroValue} + p.GetID() + p = &ProjectV2StatusUpdate{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2StatusUpdate_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2StatusUpdate{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2StatusUpdate_GetProjectNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{ProjectNodeID: &zeroValue} + p.GetProjectNodeID() + p = &ProjectV2StatusUpdate{} + p.GetProjectNodeID() + p = nil + p.GetProjectNodeID() +} + +func TestProjectV2StatusUpdate_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2StatusUpdate{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2StatusUpdate_GetStatus(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{Status: &zeroValue} + p.GetStatus() + p = &ProjectV2StatusUpdate{} + p.GetStatus() + p = nil + p.GetStatus() +} + +func TestProjectV2StatusUpdate_GetTargetDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2StatusUpdate{TargetDate: &zeroValue} + p.GetTargetDate() + p = &ProjectV2StatusUpdate{} + p.GetTargetDate() + p = nil + p.GetTargetDate() +} + +func TestProjectV2StatusUpdate_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2StatusUpdate{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2StatusUpdate{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + func TestProjectV2TextContent_GetHTML(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index 5bf246a265d..ebfe5fa1664 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -1560,17 +1560,19 @@ func TestProjectV2_String(t *testing.T) { Number: Ptr(0), ShortDescription: Ptr(""), DeletedBy: &User{}, + State: Ptr(""), + LatestStatusUpdate: &ProjectV2StatusUpdate{}, + IsTemplate: Ptr(false), URL: Ptr(""), HTMLURL: Ptr(""), ColumnsURL: Ptr(""), OwnerURL: Ptr(""), Name: Ptr(""), Body: Ptr(""), - State: Ptr(""), OrganizationPermission: Ptr(""), Private: Ptr(false), } - want := `github.ProjectV2{ID:0, NodeID:"", Owner:github.User{}, Creator:github.User{}, Title:"", Description:"", Public:false, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, DeletedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Number:0, ShortDescription:"", DeletedBy:github.User{}, URL:"", HTMLURL:"", ColumnsURL:"", OwnerURL:"", Name:"", Body:"", State:"", OrganizationPermission:"", Private:false}` + want := `github.ProjectV2{ID:0, NodeID:"", Owner:github.User{}, Creator:github.User{}, Title:"", Description:"", Public:false, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, DeletedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Number:0, ShortDescription:"", DeletedBy:github.User{}, State:"", LatestStatusUpdate:github.ProjectV2StatusUpdate{}, IsTemplate:false, URL:"", HTMLURL:"", ColumnsURL:"", OwnerURL:"", Name:"", Body:"", OrganizationPermission:"", Private:false}` if got := v.String(); got != want { t.Errorf("ProjectV2.String = %v, want %v", got, want) } diff --git a/github/projects.go b/github/projects.go index af602bc5657..d2c52f37944 100644 --- a/github/projects.go +++ b/github/projects.go @@ -7,6 +7,7 @@ package github import ( "context" + "encoding/json" "fmt" ) @@ -16,22 +17,61 @@ import ( // GitHub API docs: https://docs.github.com/rest/projects/projects type ProjectsService service +// ProjectV2ItemContentType represents the type of content in a ProjectV2Item. +type ProjectV2ItemContentType string + +// This is the set of possible content types for a ProjectV2Item. +const ( + ProjectV2ItemContentTypeDraftIssue ProjectV2ItemContentType = "DraftIssue" + ProjectV2ItemContentTypeIssue ProjectV2ItemContentType = "Issue" + ProjectV2ItemContentTypePullRequest ProjectV2ItemContentType = "PullRequest" +) + +// ProjectV2StatusUpdate represents a status update for a project. +type ProjectV2StatusUpdate struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + Creator *User `json:"creator,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + // Status can be one of: "INACTIVE", "ON_TRACK", "AT_RISK", "OFF_TRACK", "COMPLETE". + Status *string `json:"status,omitempty"` + StartDate *string `json:"start_date,omitempty"` + TargetDate *string `json:"target_date,omitempty"` + Body *string `json:"body,omitempty"` +} + +// ProjectV2DraftIssue represents a draft issue in a project. +type ProjectV2DraftIssue struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Title *string `json:"title,omitempty"` + Body *string `json:"body,omitempty"` + User *User `json:"user,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` +} + // ProjectV2 represents a v2 project. type ProjectV2 struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Owner *User `json:"owner,omitempty"` - Creator *User `json:"creator,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Public *bool `json:"public,omitempty"` - ClosedAt *Timestamp `json:"closed_at,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - DeletedAt *Timestamp `json:"deleted_at,omitempty"` - Number *int `json:"number,omitempty"` - ShortDescription *string `json:"short_description,omitempty"` - DeletedBy *User `json:"deleted_by,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Owner *User `json:"owner,omitempty"` + Creator *User `json:"creator,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Public *bool `json:"public,omitempty"` + ClosedAt *Timestamp `json:"closed_at,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + DeletedAt *Timestamp `json:"deleted_at,omitempty"` + Number *int `json:"number,omitempty"` + ShortDescription *string `json:"short_description,omitempty"` + DeletedBy *User `json:"deleted_by,omitempty"` + State *string `json:"state,omitempty"` + LatestStatusUpdate *ProjectV2StatusUpdate `json:"latest_status_update,omitempty"` + IsTemplate *bool `json:"is_template,omitempty"` // Fields migrated from the Project (classic) struct: URL *string `json:"url,omitempty"` @@ -40,7 +80,6 @@ type ProjectV2 struct { OwnerURL *string `json:"owner_url,omitempty"` Name *string `json:"name,omitempty"` Body *string `json:"body,omitempty"` - State *string `json:"state,omitempty"` OrganizationPermission *string `json:"organization_permission,omitempty"` Private *bool `json:"private,omitempty"` } @@ -115,6 +154,87 @@ type ProjectV2FieldConfiguration struct { Iterations []*ProjectV2FieldIteration `json:"iterations,omitempty"` // The list of iterations associated with the configuration. } +// ProjectV2ItemContent is a union type that holds the content of a ProjectV2Item. +// The actual type depends on the ContentType field of the parent ProjectV2Item. +// Only one of the fields will be populated after unmarshaling. +type ProjectV2ItemContent struct { + Issue *Issue `json:"-"` + PullRequest *PullRequest `json:"-"` + DraftIssue *ProjectV2DraftIssue `json:"-"` +} + +// MarshalJSON implements custom marshaling for ProjectV2ItemContent. +func (c *ProjectV2ItemContent) MarshalJSON() ([]byte, error) { + if c.Issue != nil { + return json.Marshal(c.Issue) + } + if c.PullRequest != nil { + return json.Marshal(c.PullRequest) + } + if c.DraftIssue != nil { + return json.Marshal(c.DraftIssue) + } + return []byte("null"), nil +} + +// ProjectV2Item represents a full project item with field values. +// This type is used by Get, List, and Update operations which return field values. +// The Content field is automatically unmarshaled into the appropriate type based on ContentType. +type ProjectV2Item struct { + ArchivedAt *Timestamp `json:"archived_at,omitempty"` + Content *ProjectV2ItemContent `json:"content,omitempty"` + ContentType *ProjectV2ItemContentType `json:"content_type,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + Creator *User `json:"creator,omitempty"` + Fields []*ProjectV2ItemFieldValue `json:"fields,omitempty"` + ID *int64 `json:"id,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + + // ProjectNodeID and ContentNodeID are used in ProjectsV2Item Webhook payloads. + // They may not be populated in all API responses, but are included here for completeness. + // See: https://docs.github.com/en/webhooks/webhook-events-and-payloads#projects_v2_item + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` +} + +// UnmarshalJSON implements custom unmarshaling for ProjectV2Item. +// It uses the ContentType field to determine how to unmarshal the Content field. +func (p *ProjectV2Item) UnmarshalJSON(data []byte) error { + type contentAlias ProjectV2Item + + aux := &struct { + Content json.RawMessage `json:"content,omitempty"` + *contentAlias + }{ + contentAlias: (*contentAlias)(p), + } + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + // Now unmarshal the content based on ContentType + if len(aux.Content) > 0 && string(aux.Content) != "null" && p.ContentType != nil { + p.Content = &ProjectV2ItemContent{} + switch *p.ContentType { + case ProjectV2ItemContentTypeIssue: + p.Content.Issue = &Issue{} + return json.Unmarshal(aux.Content, p.Content.Issue) + case ProjectV2ItemContentTypePullRequest: + p.Content.PullRequest = &PullRequest{} + return json.Unmarshal(aux.Content, p.Content.PullRequest) + case ProjectV2ItemContentTypeDraftIssue: + p.Content.DraftIssue = &ProjectV2DraftIssue{} + return json.Unmarshal(aux.Content, p.Content.DraftIssue) + } + } + + return nil +} + // ProjectV2Field represents a field in a GitHub Projects V2 project. // Fields define the structure and data types for project items. // @@ -131,6 +251,28 @@ type ProjectV2Field struct { UpdatedAt *Timestamp `json:"updated_at,omitempty"` } +// ProjectV2ItemFieldValue represents a field value of a project item. +type ProjectV2ItemFieldValue struct { + ID *int64 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + DataType *string `json:"data_type,omitempty"` + // Value set for the field. The type depends on the field type: + // - text: string + // - number: float64 + // - date: string (ISO 8601 date format, e.g. "2023-06-23") or null + // - single_select: object with "id", "name", "color", "description" fields or null + // - iteration: object with "id", "title", "start_date", "duration" fields or null + // - title: object with "text" field (read-only, reflects the item's title) or null + // - assignees: array of user objects with "login", "id", etc. or null + // - labels: array of label objects with "id", "name", "color", etc. or null + // - linked_pull_requests: array of pull request objects or null + // - milestone: milestone object with "id", "title", "description", etc. or null + // - repository: repository object with "id", "name", "full_name", etc. or null + // - reviewers: array of user objects or null + // - status: object with "id", "name", "color", "description" fields (same structure as single_select) or null + Value any `json:"value,omitempty"` +} + // ListOrganizationProjects lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization @@ -330,8 +472,8 @@ type GetProjectItemOptions struct { // to a project. The Type must be either "Issue" or "PullRequest" (as per API docs) and // ID is the numerical ID of that issue or pull request. type AddProjectItemOptions struct { - Type string `json:"type,omitempty"` - ID int64 `json:"id,omitempty"` + Type *ProjectV2ItemContentType `json:"type,omitempty"` + ID *int64 `json:"id,omitempty"` } // UpdateProjectV2Field represents a field update for a project item. diff --git a/github/projects_test.go b/github/projects_test.go index 4b30b3d4ecb..ddfa2bd6a4e 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -7,6 +7,7 @@ package github import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -809,7 +810,7 @@ func TestProjectsService_AddOrganizationProjectItem(t *testing.T) { }) ctx := t.Context() - item, _, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 99}) + item, _, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(99))}) if err != nil { t.Fatalf("Projects.AddOrganizationProjectItem returned error: %v", err) } @@ -829,7 +830,7 @@ func TestProjectsService_AddProjectItemForOrg_error(t *testing.T) { ctx := t.Context() const methodName = "AddOrganizationProjectItem" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 1}) + got, resp, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(1))}) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -1081,7 +1082,7 @@ func TestProjectsService_AddUserProjectItem(t *testing.T) { fmt.Fprint(w, `{"id":123,"node_id":"PVTI_new_user"}`) }) ctx := t.Context() - item, _, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "PullRequest", ID: 123}) + item, _, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("PullRequest")), ID: Ptr(int64(123))}) if err != nil { t.Fatalf("AddUserProjectItem error: %v", err) } @@ -1100,7 +1101,7 @@ func TestProjectsService_AddUserProjectItem_error(t *testing.T) { ctx := t.Context() const methodName = "AddUserProjectItem" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "Issue", ID: 5}) + got, resp, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: Ptr(ProjectV2ItemContentType("Issue")), ID: Ptr(int64(5))}) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -1298,3 +1299,366 @@ func TestProjectsService_DeleteUserProjectItem_error(t *testing.T) { return client.Projects.DeleteUserProjectItem(ctx, "u", 2, 55) }) } + +func TestProjectV2Item_UnmarshalJSON_Issue(t *testing.T) { + t.Parallel() + + // Test unmarshaling an issue + jsonData := `{ + "id": 123, + "node_id": "PVTI_test", + "content_type": "Issue", + "content": { + "id": 456, + "number": 10, + "title": "Test Issue", + "state": "open", + "body": "Issue body", + "repository": { + "id": 789, + "name": "test-repo" + } + }, + "created_at": "2023-01-01T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 123 { + t.Errorf("ID = %v, want 123", item.GetID()) + } + if item.GetNodeID() != "PVTI_test" { + t.Errorf("NodeID = %v, want PVTI_test", item.GetNodeID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypeIssue { + t.Errorf("ContentType = %v, want Issue", item.ContentType) + } + + // Verify content is unmarshaled as Issue + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetIssue() == nil { + t.Fatal("Content.Issue is nil") + } + if item.GetContent().GetIssue().GetNumber() != 10 { + t.Errorf("Issue.Number = %v, want 10", item.GetContent().GetIssue().GetNumber()) + } + if item.GetContent().GetIssue().GetTitle() != "Test Issue" { + t.Errorf("Issue.Title = %v, want Test Issue", item.GetContent().GetIssue().GetTitle()) + } + if item.GetContent().GetIssue().GetState() != "open" { + t.Errorf("Issue.State = %v, want open", item.GetContent().GetIssue().GetState()) + } + + // Verify other content types are nil + if item.GetContent().GetPullRequest() != nil { + t.Error("Content.PullRequest should be nil for Issue content") + } + if item.GetContent().GetDraftIssue() != nil { + t.Error("Content.DraftIssue should be nil for Issue content") + } +} + +func TestProjectV2Item_UnmarshalJSON_PullRequest(t *testing.T) { + t.Parallel() + + // Test unmarshaling a pull request + jsonData := `{ + "id": 124, + "node_id": "PVTI_pr", + "content_type": "PullRequest", + "content": { + "id": 457, + "number": 20, + "title": "Test PR", + "state": "closed", + "merged": true, + "merge_commit_sha": "abc123", + "head": { + "ref": "feature-branch", + "sha": "def456" + }, + "base": { + "ref": "main", + "sha": "ghi789" + } + }, + "created_at": "2023-01-02T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 124 { + t.Errorf("ID = %v, want 124", item.GetID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypePullRequest { + t.Errorf("ContentType = %v, want PullRequest", item.ContentType) + } + + // Verify content is unmarshaled as PullRequest + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetPullRequest() == nil { + t.Fatal("Content.PullRequest is nil") + } + if item.GetContent().GetPullRequest().GetNumber() != 20 { + t.Errorf("PullRequest.Number = %v, want 20", item.GetContent().GetPullRequest().GetNumber()) + } + if item.GetContent().GetPullRequest().GetTitle() != "Test PR" { + t.Errorf("PullRequest.Title = %v, want Test PR", item.GetContent().GetPullRequest().GetTitle()) + } + if !item.GetContent().GetPullRequest().GetMerged() { + t.Errorf("PullRequest.Merged = %t, want true", item.GetContent().GetPullRequest().GetMerged()) + } + if item.GetContent().GetPullRequest().GetMergeCommitSHA() != "abc123" { + t.Errorf("PullRequest.MergeCommitSHA = %v, want abc123", item.GetContent().GetPullRequest().GetMergeCommitSHA()) + } + + // Verify other content types are nil + if item.GetContent().GetIssue() != nil { + t.Error("Content.Issue should be nil for PullRequest content") + } + if item.GetContent().GetDraftIssue() != nil { + t.Error("Content.DraftIssue should be nil for PullRequest content") + } +} + +func TestProjectV2Item_UnmarshalJSON_DraftIssue(t *testing.T) { + t.Parallel() + + // Test unmarshaling a draft issue + jsonData := `{ + "id": 125, + "node_id": "PVTI_draft", + "content_type": "DraftIssue", + "content": { + "id": 458, + "node_id": "DI_test", + "title": "Draft Issue Title", + "body": "Draft issue body content" + }, + "created_at": "2023-01-03T00:00:00Z" + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Verify basic fields + if item.GetID() != 125 { + t.Errorf("ID = %v, want 125", item.GetID()) + } + if item.ContentType == nil || *item.ContentType != ProjectV2ItemContentTypeDraftIssue { + t.Errorf("ContentType = %v, want DraftIssue", item.ContentType) + } + + // Verify content is unmarshaled as DraftIssue + if item.Content == nil { + t.Fatal("Content is nil") + } + if item.GetContent().GetDraftIssue() == nil { + t.Fatal("Content.DraftIssue is nil") + } + if item.GetContent().GetDraftIssue().GetID() != 458 { + t.Errorf("DraftIssue.ID = %v, want 458", item.GetContent().GetDraftIssue().GetID()) + } + if item.GetContent().GetDraftIssue().GetTitle() != "Draft Issue Title" { + t.Errorf("DraftIssue.Title = %v, want Draft Issue Title", item.GetContent().GetDraftIssue().GetTitle()) + } + if item.GetContent().GetDraftIssue().GetBody() != "Draft issue body content" { + t.Errorf("DraftIssue.Body = %v, want Draft issue body content", item.GetContent().GetDraftIssue().GetBody()) + } + + // Verify other content types are nil + if item.GetContent().GetIssue() != nil { + t.Error("Content.Issue should be nil for DraftIssue content") + } + if item.GetContent().GetPullRequest() != nil { + t.Error("Content.PullRequest should be nil for DraftIssue content") + } +} + +func TestProjectV2Item_UnmarshalJSON_NullContent(t *testing.T) { + t.Parallel() + + // Test with null content + jsonData := `{ + "id": 126, + "node_id": "PVTI_null", + "content_type": "Issue", + "content": null + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Content should be nil + if item.Content != nil { + t.Error("Content should be nil when content is null in JSON") + } +} + +func TestProjectV2Item_UnmarshalJSON_MissingContentType(t *testing.T) { + t.Parallel() + + // Test without content_type field + jsonData := `{ + "id": 127, + "node_id": "PVTI_no_type", + "content": { + "id": 459, + "title": "Some content" + } + }` + + var item ProjectV2Item + if err := json.Unmarshal([]byte(jsonData), &item); err != nil { + t.Fatalf("json.Unmarshal failed: %v", err) + } + + // Should handle missing ContentType gracefully - content should be nil + // since we can't determine the type + if item.Content != nil { + t.Error("Content should be nil when ContentType is missing") + } +} + +func TestProjectV2Item_UnmarshalJSON_EmptyJSON(t *testing.T) { + t.Parallel() + + // Test with null JSON + var item ProjectV2Item + if err := json.Unmarshal([]byte("null"), &item); err != nil { + t.Fatalf("json.Unmarshal failed with null: %v", err) + } + + // Verify item is in zero state after unmarshaling null + if item.Content != nil { + t.Error("Content should be nil after unmarshaling null") + } +} + +func TestProjectV2Item_UnmarshalJSON_InvalidJSON(t *testing.T) { + t.Parallel() + + // Test with invalid JSON + var item ProjectV2Item + if err := json.Unmarshal([]byte("~~~"), &item); err == nil { + t.Error("expected error for invalid JSON, got nil") + } +} + +func TestProjectV2Item_Marshal_Issue(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeIssue), + Content: &ProjectV2ItemContent{ + Issue: &Issue{ + Number: Ptr(42), + Title: Ptr("Bug report"), + State: Ptr("open"), + }, + }, + ID: Ptr(int64(123)), + } + + want := `{ + "content_type":"Issue", + "content":{ + "number":42, + "state":"open", + "title":"Bug report" + }, + "id":123 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_PullRequest(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypePullRequest), + Content: &ProjectV2ItemContent{ + PullRequest: &PullRequest{ + Number: Ptr(99), + Title: Ptr("Feature addition"), + State: Ptr("closed"), + }, + }, + ID: Ptr(int64(456)), + } + + want := `{ + "content_type":"PullRequest", + "content":{ + "number":99, + "state":"closed", + "title":"Feature addition" + }, + "id":456 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_DraftIssue(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Item{}, "{}") + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue), + Content: &ProjectV2ItemContent{ + DraftIssue: &ProjectV2DraftIssue{ + Title: Ptr("Draft task"), + Body: Ptr("Work in progress"), + }, + }, + ID: Ptr(int64(789)), + } + + want := `{ + "content_type":"DraftIssue", + "content":{ + "body":"Work in progress", + "title":"Draft task" + }, + "id":789 + }` + + testJSONMarshal(t, item, want) +} + +func TestProjectV2Item_Marshal_MissingContent(t *testing.T) { + t.Parallel() + + item := &ProjectV2Item{ + ContentType: Ptr(ProjectV2ItemContentTypeIssue), + Content: nil, + ID: Ptr(int64(789)), + } + + want := `{ + "content_type":"Issue", + "id":789 + }` + + testJSONMarshal(t, item, want) +} From 67e5f7b5edab41de76599ebca0687aa976b619bf Mon Sep 17 00:00:00 2001 From: Alejandro <60017052+elminster-aom@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:27:50 +0100 Subject: [PATCH 45/49] feat: Implement Enterprise SCIM - Get a Group or User (#3889) --- github/enterprise_scim.go | 69 ++++++++++++- github/enterprise_scim_test.go | 165 ++++++++++++++++++++++++++++++++ github/github-accessors.go | 8 ++ github/github-accessors_test.go | 11 +++ 4 files changed, 250 insertions(+), 3 deletions(-) diff --git a/github/enterprise_scim.go b/github/enterprise_scim.go index bf8e6429365..ea45b2e5c86 100644 --- a/github/enterprise_scim.go +++ b/github/enterprise_scim.go @@ -70,7 +70,7 @@ type ListProvisionedSCIMGroupsEnterpriseOptions struct { // If specified, only results that match the specified filter will be returned. // Possible filters are `externalId`, `id`, and `displayName`. For example, `externalId eq "a123"`. Filter *string `url:"filter,omitempty"` - // Excludes the specified attribute from being returned in the results. + // Excludes the specified attributes from being returned in the results. ExcludedAttributes *string `url:"excludedAttributes,omitempty"` // Used for pagination: the starting index of the first result to return when paginating through values. // Default: 1. @@ -80,6 +80,14 @@ type ListProvisionedSCIMGroupsEnterpriseOptions struct { Count *int `url:"count,omitempty"` } +// GetProvisionedSCIMGroupEnterpriseOptions represents query parameters for GetProvisionedSCIMGroup. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#get-scim-provisioning-information-for-an-enterprise-group +type GetProvisionedSCIMGroupEnterpriseOptions struct { + // Excludes the specified attributes from being returned in the results. + ExcludedAttributes *string `url:"excludedAttributes,omitempty"` +} + // SCIMEnterpriseUserAttributes represents supported SCIM enterprise user attributes, and represents the result of calling UpdateSCIMUserAttribute. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-user-attributes @@ -162,8 +170,9 @@ type SCIMEnterpriseAttributeOperation struct { // ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise. // -// You can improve query search time by using the `excludedAttributes` query -// parameter with a value of `members` to exclude members from the response. +// You can improve query search time by using the `excludedAttributes` and +// exclude the specified attributes, e.g. `members` to exclude members from the +// response. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise // @@ -381,6 +390,60 @@ func (s *EnterpriseService) ProvisionSCIMUser(ctx context.Context, enterprise st return userProvisioned, resp, nil } +// GetProvisionedSCIMGroup gets information about a SCIM group. +// +// You can use the `excludedAttributes` from `opts` and exclude the specified +// attributes from being returned in the results. Using this parameter can +// speed up response time. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#get-scim-provisioning-information-for-an-enterprise-group +// +//meta:operation GET /scim/v2/enterprises/{enterprise}/Groups/{scim_group_id} +func (s *EnterpriseService) GetProvisionedSCIMGroup(ctx context.Context, enterprise, scimGroupID string, opts *GetProvisionedSCIMGroupEnterpriseOptions) (*SCIMEnterpriseGroupAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Groups/%v", enterprise, scimGroupID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + group := new(SCIMEnterpriseGroupAttributes) + resp, err := s.client.Do(ctx, req, group) + if err != nil { + return nil, resp, err + } + + return group, resp, nil +} + +// GetProvisionedSCIMUser gets information about a SCIM user. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#get-scim-provisioning-information-for-an-enterprise-user +// +//meta:operation GET /scim/v2/enterprises/{enterprise}/Users/{scim_user_id} +func (s *EnterpriseService) GetProvisionedSCIMUser(ctx context.Context, enterprise, scimUserID string) (*SCIMEnterpriseUserAttributes, *Response, error) { + u := fmt.Sprintf("scim/v2/enterprises/%v/Users/%v", enterprise, scimUserID) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeSCIM) + + user := new(SCIMEnterpriseUserAttributes) + resp, err := s.client.Do(ctx, req, user) + if err != nil { + return nil, resp, err + } + + return user, resp, nil +} + // DeleteSCIMGroup deletes a SCIM group from an enterprise. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#delete-a-scim-group-from-an-enterprise diff --git a/github/enterprise_scim_test.go b/github/enterprise_scim_test.go index a0f984f818f..edfbd64724d 100644 --- a/github/enterprise_scim_test.go +++ b/github/enterprise_scim_test.go @@ -182,6 +182,15 @@ func TestListProvisionedSCIMGroupsEnterpriseOptions_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } +func TestGetProvisionedSCIMGroupEnterpriseOptions_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &GetProvisionedSCIMGroupEnterpriseOptions{}, "{}") + + u := &GetProvisionedSCIMGroupEnterpriseOptions{ExcludedAttributes: Ptr("ea")} + want := `{"excludedAttributes": "ea"}` + testJSONMarshal(t, u, want) +} + func TestListProvisionedSCIMUsersEnterpriseOptions_Marshal(t *testing.T) { t.Parallel() testJSONMarshal(t, &ListProvisionedSCIMUsersEnterpriseOptions{}, "{}") @@ -1014,6 +1023,162 @@ func TestEnterpriseService_ProvisionSCIMUser(t *testing.T) { }) } +func TestEnterpriseService_GetProvisionedSCIMGroup(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Groups/914a", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeSCIM) + testFormValues(t, r, values{"excludedAttributes": "members,meta"}) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesGroups+`"], + "id": "914a", + "externalId": "de88", + "displayName": "gn1", + "meta": { + "resourceType": "Group", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.com/scim/v2/enterprises/ee/Groups/914a" + }, + "members": [{ + "value": "e7f9", + "$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9", + "display": "d1" + }] + }`) + }) + + ctx := t.Context() + opts := &GetProvisionedSCIMGroupEnterpriseOptions{ExcludedAttributes: Ptr("members,meta")} + got, _, err := client.Enterprise.GetProvisionedSCIMGroup(ctx, "ee", "914a", opts) + if err != nil { + t.Fatalf("Enterprise.GetProvisionedSCIMGroup returned unexpected error: %v", err) + } + + want := &SCIMEnterpriseGroupAttributes{ + ID: Ptr("914a"), + Meta: &SCIMEnterpriseMeta{ + ResourceType: "Group", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/Groups/914a"), + }, + DisplayName: Ptr("gn1"), + Schemas: []string{SCIMSchemasURINamespacesGroups}, + ExternalID: Ptr("de88"), + Members: []*SCIMEnterpriseDisplayReference{{ + Value: "e7f9", + Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/e7f9"), + Display: Ptr("d1"), + }}, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.GetProvisionedSCIMGroup diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "GetProvisionedSCIMGroup" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.GetProvisionedSCIMGroup(ctx, "ee", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetProvisionedSCIMGroup(ctx, "ee", "914a", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_GetProvisionedSCIMUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/scim/v2/enterprises/ee/Users/5fc0", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeSCIM) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "schemas": ["`+SCIMSchemasURINamespacesUser+`"], + "id": "5fc0", + "externalId": "00u1", + "userName": "octocat@github.com", + "displayName": "Mona Octocat", + "name": { + "givenName": "Mona", + "familyName": "Octocat", + "formatted": "Mona Octocat" + }, + "emails": [ + { + "value": "octocat@github.com", + "primary": true + } + ], + "active": true, + "meta": { + "resourceType": "User", + "created": `+referenceTimeStr+`, + "lastModified": `+referenceTimeStr+`, + "location": "https://api.github.com/scim/v2/enterprises/ee/Users/5fc0" + } + }`) + }) + + ctx := t.Context() + got, _, err := client.Enterprise.GetProvisionedSCIMUser(ctx, "ee", "5fc0") + if err != nil { + t.Fatalf("Enterprise.GetProvisionedSCIMUser returned unexpected error: %v", err) + } + + want := &SCIMEnterpriseUserAttributes{ + Schemas: []string{SCIMSchemasURINamespacesUser}, + ID: Ptr("5fc0"), + ExternalID: "00u1", + UserName: "octocat@github.com", + DisplayName: "Mona Octocat", + Name: &SCIMEnterpriseUserName{ + GivenName: "Mona", + FamilyName: "Octocat", + Formatted: Ptr("Mona Octocat"), + }, + Emails: []*SCIMEnterpriseUserEmail{{ + Value: "octocat@github.com", + Primary: true, + }}, + Active: true, + Meta: &SCIMEnterpriseMeta{ + ResourceType: "User", + Created: &Timestamp{referenceTime}, + LastModified: &Timestamp{referenceTime}, + Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/5fc0"), + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("Enterprise.GetProvisionedSCIMUser diff mismatch (-want +got):\n%v", diff) + } + + const methodName = "GetProvisionedSCIMUser" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.GetProvisionedSCIMUser(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.GetProvisionedSCIMUser(ctx, "ee", "5fc0") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestEnterpriseService_DeleteSCIMGroup(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/github-accessors.go b/github/github-accessors.go index b907c2af56a..dbf8d5dc6de 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -10238,6 +10238,14 @@ func (g *GetAuditLogOptions) GetPhrase() string { return *g.Phrase } +// GetExcludedAttributes returns the ExcludedAttributes field if it's non-nil, zero value otherwise. +func (g *GetProvisionedSCIMGroupEnterpriseOptions) GetExcludedAttributes() string { + if g == nil || g.ExcludedAttributes == nil { + return "" + } + return *g.ExcludedAttributes +} + // GetComments returns the Comments field if it's non-nil, zero value otherwise. func (g *Gist) GetComments() int { if g == nil || g.Comments == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index b12c405d2bf..6ed08a7fac5 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -13275,6 +13275,17 @@ func TestGetAuditLogOptions_GetPhrase(tt *testing.T) { g.GetPhrase() } +func TestGetProvisionedSCIMGroupEnterpriseOptions_GetExcludedAttributes(tt *testing.T) { + tt.Parallel() + var zeroValue string + g := &GetProvisionedSCIMGroupEnterpriseOptions{ExcludedAttributes: &zeroValue} + g.GetExcludedAttributes() + g = &GetProvisionedSCIMGroupEnterpriseOptions{} + g.GetExcludedAttributes() + g = nil + g.GetExcludedAttributes() +} + func TestGist_GetComments(tt *testing.T) { tt.Parallel() var zeroValue int From e85e1dc3e7c3f925b39ce587dc81d5b814dafd93 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Mon, 22 Dec 2025 19:07:08 +0530 Subject: [PATCH 46/49] feat: Add support for Codespace Machines APIs (#3890) --- github/codespaces_machines.go | 74 ++++++++++++++ github/codespaces_machines_test.go | 149 +++++++++++++++++++++++++++++ github/github-accessors.go | 24 +++++ github/github-accessors_test.go | 33 +++++++ 4 files changed, 280 insertions(+) create mode 100644 github/codespaces_machines.go create mode 100644 github/codespaces_machines_test.go diff --git a/github/codespaces_machines.go b/github/codespaces_machines.go new file mode 100644 index 00000000000..5277b1f4098 --- /dev/null +++ b/github/codespaces_machines.go @@ -0,0 +1,74 @@ +// 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" +) + +// CodespacesMachines represent a list of machines. +type CodespacesMachines struct { + TotalCount int64 `json:"total_count"` + Machines []*CodespacesMachine `json:"machines"` +} + +// ListRepoMachineTypesOptions represent options for ListMachineTypesForRepository. +type ListRepoMachineTypesOptions struct { + // Ref represent the branch or commit to check for prebuild availability and devcontainer restrictions. + Ref *string `url:"ref,omitempty"` + // Location represent the location to check for available machines. Assigned by IP if not provided. + Location *string `url:"location,omitempty"` + // ClientIP represent the IP for location auto-detection when proxying a request + ClientIP *string `url:"client_ip,omitempty"` +} + +// ListRepositoryMachineTypes lists the machine types available for a given repository based on its configuration. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/machines#list-available-machine-types-for-a-repository +// +//meta:operation GET /repos/{owner}/{repo}/codespaces/machines +func (s *CodespacesService) ListRepositoryMachineTypes(ctx context.Context, owner, repo string, opts *ListRepoMachineTypesOptions) (*CodespacesMachines, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/codespaces/machines", owner, repo) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var machines *CodespacesMachines + resp, err := s.client.Do(ctx, req, &machines) + if err != nil { + return nil, resp, err + } + + return machines, resp, nil +} + +// ListCodespaceMachineTypes lists the machine types a codespace can transition to use. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/machines#list-machine-types-for-a-codespace +// +//meta:operation GET /user/codespaces/{codespace_name}/machines +func (s *CodespacesService) ListCodespaceMachineTypes(ctx context.Context, codespaceName string) (*CodespacesMachines, *Response, error) { + u := fmt.Sprintf("user/codespaces/%v/machines", codespaceName) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var machines *CodespacesMachines + resp, err := s.client.Do(ctx, req, &machines) + if err != nil { + return nil, resp, err + } + + return machines, resp, nil +} diff --git a/github/codespaces_machines_test.go b/github/codespaces_machines_test.go new file mode 100644 index 00000000000..d413d168b93 --- /dev/null +++ b/github/codespaces_machines_test.go @@ -0,0 +1,149 @@ +// 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 ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCodespacesService_ListRepositoryMachineTypes(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/owner/repo/codespaces/machines", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "ref": "main", + "location": "WestUs2", + "client_ip": "1.2.3.4", + }) + fmt.Fprint(w, `{ + "total_count": 1, + "machines": [ + { + "name": "standardLinux", + "display_name": "4 cores, 8 GB RAM, 64 GB storage", + "operating_system": "linux", + "storage_in_bytes": 68719476736, + "memory_in_bytes": 17179869184, + "cpus": 4, + "prebuild_availability": "ready" + } + ] + }`) + }) + + ctx := t.Context() + opts := &ListRepoMachineTypesOptions{ + Ref: Ptr("main"), + Location: Ptr("WestUs2"), + ClientIP: Ptr("1.2.3.4"), + } + + got, _, err := client.Codespaces.ListRepositoryMachineTypes( + ctx, + "owner", + "repo", + opts, + ) + if err != nil { + t.Fatalf("Codespaces.ListRepositoryMachineTypes returned error: %v", err) + } + + want := &CodespacesMachines{ + TotalCount: 1, + Machines: []*CodespacesMachine{ + { + Name: Ptr("standardLinux"), + DisplayName: Ptr("4 cores, 8 GB RAM, 64 GB storage"), + OperatingSystem: Ptr("linux"), + StorageInBytes: Ptr(int64(68719476736)), + MemoryInBytes: Ptr(int64(17179869184)), + CPUs: Ptr(4), + PrebuildAvailability: Ptr("ready"), + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListRepositoryMachineTypes returned %+v, want %+v", got, want) + } + + const methodName = "ListRepositoryMachineTypes" + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListRepositoryMachineTypes(ctx, "\n", "/n", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListRepositoryMachineTypes(ctx, "/n", "/n", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_ListCodespaceMachineTypes(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/user/codespaces/codespace_1/machines", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + + fmt.Fprint(w, `{ + "total_count": 1, + "machines": [ + { + "name": "standardLinux", + "display_name": "4 cores, 8 GB RAM, 64 GB storage", + "operating_system": "linux", + "storage_in_bytes": 68719476736, + "memory_in_bytes": 17179869184, + "cpus": 4, + "prebuild_availability": "ready" + } + ] + }`) + }) + + ctx := t.Context() + got, _, err := client.Codespaces.ListCodespaceMachineTypes(ctx, "codespace_1") + if err != nil { + t.Fatalf("Codespaces.ListCodespaceMachineTypes returned error: %v", err) + } + + want := &CodespacesMachines{ + TotalCount: 1, + Machines: []*CodespacesMachine{ + { + Name: Ptr("standardLinux"), + DisplayName: Ptr("4 cores, 8 GB RAM, 64 GB storage"), + OperatingSystem: Ptr("linux"), + StorageInBytes: Ptr(int64(68719476736)), + MemoryInBytes: Ptr(int64(17179869184)), + CPUs: Ptr(4), + PrebuildAvailability: Ptr("ready"), + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListCodespaceMachineTypes returned %+v, want %+v", got, want) + } + + const methodName = "ListCodespaceMachineTypes" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListCodespaceMachineTypes(ctx, "/n") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index dbf8d5dc6de..35d86729d03 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -14686,6 +14686,30 @@ func (l *ListProvisionedSCIMUsersEnterpriseOptions) GetStartIndex() int { return *l.StartIndex } +// GetClientIP returns the ClientIP field if it's non-nil, zero value otherwise. +func (l *ListRepoMachineTypesOptions) GetClientIP() string { + if l == nil || l.ClientIP == nil { + return "" + } + return *l.ClientIP +} + +// GetLocation returns the Location field if it's non-nil, zero value otherwise. +func (l *ListRepoMachineTypesOptions) GetLocation() string { + if l == nil || l.Location == nil { + return "" + } + return *l.Location +} + +// GetRef returns the Ref field if it's non-nil, zero value otherwise. +func (l *ListRepoMachineTypesOptions) GetRef() string { + if l == nil || l.Ref == nil { + return "" + } + return *l.Ref +} + // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. func (l *ListRepositories) GetTotalCount() int { if l == nil || l.TotalCount == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 6ed08a7fac5..3dcbd7129c4 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -19106,6 +19106,39 @@ func TestListProvisionedSCIMUsersEnterpriseOptions_GetStartIndex(tt *testing.T) l.GetStartIndex() } +func TestListRepoMachineTypesOptions_GetClientIP(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListRepoMachineTypesOptions{ClientIP: &zeroValue} + l.GetClientIP() + l = &ListRepoMachineTypesOptions{} + l.GetClientIP() + l = nil + l.GetClientIP() +} + +func TestListRepoMachineTypesOptions_GetLocation(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListRepoMachineTypesOptions{Location: &zeroValue} + l.GetLocation() + l = &ListRepoMachineTypesOptions{} + l.GetLocation() + l = nil + l.GetLocation() +} + +func TestListRepoMachineTypesOptions_GetRef(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListRepoMachineTypesOptions{Ref: &zeroValue} + l.GetRef() + l = &ListRepoMachineTypesOptions{} + l.GetRef() + l = nil + l.GetRef() +} + func TestListRepositories_GetTotalCount(tt *testing.T) { tt.Parallel() var zeroValue int From 34bb84825d0a00b7ccfa15d958a5e7e0b7014878 Mon Sep 17 00:00:00 2001 From: Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:21:11 +0530 Subject: [PATCH 47/49] feat!: Use `omitzero` for `BypassActors` to support handling empty arrays (#3891) BREAKING CHANGE: `UpdateRepositoryRulesetClearBypassActor`, `UpdateRepositoryRulesetClearBypassActor`, `UpdateRulesetClearBypassActor`, and `UpdateRulesetNoBypassActor` have been removed as they are no longer needed. --- github/enterprise_rules.go | 27 ------ github/enterprise_rules_test.go | 123 +++++++++++------------ github/orgs_rules.go | 27 ------ github/orgs_rules_test.go | 85 +++++++++------- github/repos_rules.go | 90 ----------------- github/repos_rules_test.go | 167 +++++++++++++------------------- github/rules.go | 2 +- 7 files changed, 176 insertions(+), 345 deletions(-) diff --git a/github/enterprise_rules.go b/github/enterprise_rules.go index 33485824f7e..3cd3ee4e85e 100644 --- a/github/enterprise_rules.go +++ b/github/enterprise_rules.go @@ -76,33 +76,6 @@ func (s *EnterpriseService) UpdateRepositoryRuleset(ctx context.Context, enterpr return rs, resp, nil } -// UpdateRepositoryRulesetClearBypassActor clears the bypass actors for a repository ruleset for the specified enterprise. -// -// This function is necessary as the UpdateRepositoryRuleset function does not marshal ByPassActor if passed as an empty array. -// -// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/rules#update-an-enterprise-repository-ruleset -// -//meta:operation PUT /enterprises/{enterprise}/rulesets/{ruleset_id} -func (s *EnterpriseService) UpdateRepositoryRulesetClearBypassActor(ctx context.Context, enterprise string, rulesetID int64) (*Response, error) { - u := fmt.Sprintf("enterprises/%v/rulesets/%v", enterprise, rulesetID) - - rsClearBypassActor := rulesetClearBypassActors{ - BypassActors: []*BypassActor{}, - } - - req, err := s.client.NewRequest("PUT", u, rsClearBypassActor) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(ctx, req, nil) - if err != nil { - return resp, err - } - - return resp, nil -} - // DeleteRepositoryRuleset deletes a repository ruleset from the specified enterprise. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/rules#delete-an-enterprise-repository-ruleset diff --git a/github/enterprise_rules_test.go b/github/enterprise_rules_test.go index b72c758a53b..724a2e61036 100644 --- a/github/enterprise_rules_test.go +++ b/github/enterprise_rules_test.go @@ -6,6 +6,7 @@ package github import ( + "encoding/json" "fmt" "net/http" "testing" @@ -384,6 +385,61 @@ func TestEnterpriseService_CreateRepositoryRuleset_OrgNameRepoName(t *testing.T) }) } +func TestEnterpriseService_UpdateRepositoryRuleset_OmitZero_Nil(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/rulesets/84", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Errorf("could not decode body: %v", err) + } + + if _, ok := v["bypass_actors"]; ok { + t.Error("Request body contained 'bypass_actors', expected it to be omitted") + } + + fmt.Fprint(w, `{"id": 84, "name": "test ruleset"}`) + }) + + ctx := t.Context() + input := RepositoryRuleset{ + Name: "test ruleset", + BypassActors: nil, + } + + _, _, err := client.Enterprise.UpdateRepositoryRuleset(ctx, "e", 84, input) + if err != nil { + t.Errorf("Enterprise.UpdateRepositoryRuleset returned error: %v", err) + } +} + +func TestEnterpriseService_UpdateRepositoryRuleset_OmitZero_EmptySlice(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/rulesets/84", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + testBody(t, r, `{"name":"test ruleset","source":"","enforcement":"","bypass_actors":[]}`+"\n") + + fmt.Fprint(w, `{"id": 84, "name": "test ruleset", "bypass_actors": []}`) + }) + + ctx := t.Context() + input := RepositoryRuleset{ + Name: "test ruleset", + BypassActors: []*BypassActor{}, + } + + _, _, err := client.Enterprise.UpdateRepositoryRuleset(ctx, "e", 84, input) + if err != nil { + t.Errorf("Enterprise.UpdateRepositoryRuleset returned error: %v", err) + } +} + func TestEnterpriseService_CreateRepositoryRuleset_OrgNameRepoProperty(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -1769,73 +1825,6 @@ func TestEnterpriseService_UpdateRepositoryRuleset(t *testing.T) { }) } -func TestEnterpriseService_UpdateRepositoryRulesetClearBypassActor(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - mux.HandleFunc("/enterprises/e/rulesets/84", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "PUT") - testBody(t, r, `{"bypass_actors":[]}`+"\n") - fmt.Fprint(w, `{ - "id": 84, - "name": "test ruleset", - "target": "branch", - "source_type": "Enterprise", - "source": "e", - "enforcement": "active", - "bypass_mode": "none", - "conditions": { - "organization_name": { - "include": [ - "important_organization", - "another_important_organization" - ], - "exclude": [ - "unimportant_organization" - ] - }, - "repository_name": { - "include": [ - "important_repository", - "another_important_repository" - ], - "exclude": [ - "unimportant_repository" - ], - "protected": true - }, - "ref_name": { - "include": [ - "refs/heads/main", - "refs/heads/master" - ], - "exclude": [ - "refs/heads/dev*" - ] - } - }, - "rules": [ - { - "type": "creation" - } - ] - }`) - }) - - ctx := t.Context() - - _, err := client.Enterprise.UpdateRepositoryRulesetClearBypassActor(ctx, "e", 84) - if err != nil { - t.Errorf("Enterprise.UpdateRepositoryRulesetClearBypassActor returned error: %v \n", err) - } - - const methodName = "UpdateRepositoryRulesetClearBypassActor" - - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - return client.Enterprise.UpdateRepositoryRulesetClearBypassActor(ctx, "e", 84) - }) -} - func TestEnterpriseService_DeleteRepositoryRuleset(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/orgs_rules.go b/github/orgs_rules.go index 2b6a79387e3..26012091a46 100644 --- a/github/orgs_rules.go +++ b/github/orgs_rules.go @@ -103,33 +103,6 @@ func (s *OrganizationsService) UpdateRepositoryRuleset(ctx context.Context, org return rs, resp, nil } -// UpdateRepositoryRulesetClearBypassActor clears the bypass actors for a repository ruleset for the specified organization. -// -// This function is necessary as the UpdateRepositoryRuleset function does not marshal ByPassActor if passed as an empty array. -// -// GitHub API docs: https://docs.github.com/rest/orgs/rules#update-an-organization-repository-ruleset -// -//meta:operation PUT /orgs/{org}/rulesets/{ruleset_id} -func (s *OrganizationsService) UpdateRepositoryRulesetClearBypassActor(ctx context.Context, org string, rulesetID int64) (*Response, error) { - u := fmt.Sprintf("orgs/%v/rulesets/%v", org, rulesetID) - - rsClearBypassActor := rulesetClearBypassActors{ - BypassActors: []*BypassActor{}, - } - - req, err := s.client.NewRequest("PUT", u, rsClearBypassActor) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(ctx, req, nil) - if err != nil { - return resp, err - } - - return resp, nil -} - // DeleteRepositoryRuleset deletes a repository ruleset from the specified organization. // // GitHub API docs: https://docs.github.com/rest/orgs/rules#delete-an-organization-repository-ruleset diff --git a/github/orgs_rules_test.go b/github/orgs_rules_test.go index 03f634ab4d8..1c70947931e 100644 --- a/github/orgs_rules_test.go +++ b/github/orgs_rules_test.go @@ -6,6 +6,7 @@ package github import ( + "encoding/json" "fmt" "net/http" "testing" @@ -1587,62 +1588,74 @@ func TestOrganizationsService_UpdateRepositoryRulesetWithRepoProp(t *testing.T) }) } -func TestOrganizationsService_UpdateRepositoryRulesetClearBypassActor(t *testing.T) { +func TestOrganizationsService_UpdateRepositoryRuleset_OmitZero_Nil(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/orgs/o/rulesets/21", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "PUT") - testBody(t, r, `{"bypass_actors":[]}`+"\n") + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Errorf("could not decode body: %v", err) + } + + if _, ok := v["bypass_actors"]; ok { + t.Error("Request body contained 'bypass_actors', expected it to be omitted for nil input") + } + fmt.Fprint(w, `{ "id": 21, "name": "test ruleset", - "target": "branch", "source_type": "Organization", "source": "o", - "enforcement": "active", - "bypass_mode": "none", - "conditions": { - "repository_name": { - "include": [ - "important_repository", - "another_important_repository" - ], - "exclude": [ - "unimportant_repository" - ], - "protected": true - }, - "ref_name": { - "include": [ - "refs/heads/main", - "refs/heads/master" - ], - "exclude": [ - "refs/heads/dev*" - ] - } - }, - "rules": [ - { - "type": "creation" - } - ] + "enforcement": "active" }`) }) ctx := t.Context() + input := RepositoryRuleset{ + Name: "test ruleset", + Enforcement: RulesetEnforcementActive, + BypassActors: nil, + } - _, err := client.Organizations.UpdateRepositoryRulesetClearBypassActor(ctx, "o", 21) + _, _, err := client.Organizations.UpdateRepositoryRuleset(ctx, "o", 21, input) if err != nil { - t.Errorf("Organizations.UpdateRepositoryRulesetClearBypassActor returned error: %v \n", err) + t.Errorf("Organizations.UpdateRepositoryRuleset returned error: %v", err) } +} - const methodName = "UpdateRepositoryRulesetClearBypassActor" +func TestOrganizationsService_UpdateRepositoryRuleset_OmitZero_EmptySlice(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - return client.Organizations.UpdateRepositoryRulesetClearBypassActor(ctx, "o", 21) + mux.HandleFunc("/orgs/o/rulesets/21", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + testBody(t, r, `{"name":"test ruleset","source":"","enforcement":"active","bypass_actors":[]}`+"\n") + + fmt.Fprint(w, `{ + "id": 21, + "name": "test ruleset", + "source_type": "Organization", + "source": "o", + "enforcement": "active", + "bypass_actors": [] + }`) }) + + ctx := t.Context() + input := RepositoryRuleset{ + Name: "test ruleset", + Enforcement: RulesetEnforcementActive, + BypassActors: []*BypassActor{}, + } + + _, _, err := client.Organizations.UpdateRepositoryRuleset(ctx, "o", 21, input) + if err != nil { + t.Errorf("Organizations.UpdateRepositoryRuleset returned error: %v", err) + } } func TestOrganizationsService_DeleteRepositoryRuleset(t *testing.T) { diff --git a/github/repos_rules.go b/github/repos_rules.go index 8c1f4980e3f..b02df31316d 100644 --- a/github/repos_rules.go +++ b/github/repos_rules.go @@ -10,29 +10,6 @@ import ( "fmt" ) -// rulesetNoOmitBypassActors represents a GitHub ruleset object. The struct does not omit bypassActors if the field is nil or an empty array is passed. -type rulesetNoOmitBypassActors struct { - ID *int64 `json:"id,omitempty"` - Name string `json:"name"` - Target *RulesetTarget `json:"target,omitempty"` - SourceType *RulesetSourceType `json:"source_type,omitempty"` - Source string `json:"source"` - Enforcement RulesetEnforcement `json:"enforcement"` - BypassActors []*BypassActor `json:"bypass_actors"` - CurrentUserCanBypass *BypassMode `json:"current_user_can_bypass,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Links *RepositoryRulesetLinks `json:"_links,omitempty"` - Conditions *RepositoryRulesetConditions `json:"conditions,omitempty"` - Rules *RepositoryRulesetRules `json:"rules,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` -} - -// rulesetClearBypassActors is used to clear the bypass actors when modifying a GitHub ruleset object. -type rulesetClearBypassActors struct { - BypassActors []*BypassActor `json:"bypass_actors"` -} - // GetRulesForBranch gets all the repository rules that apply to the specified branch. // // GitHub API docs: https://docs.github.com/rest/repos/rules#get-rules-for-a-branch @@ -164,73 +141,6 @@ func (s *RepositoriesService) UpdateRuleset(ctx context.Context, owner, repo str return rs, resp, nil } -// UpdateRulesetClearBypassActor clears the bypass actors for a repository ruleset for the specified repository. -// -// This function is necessary as the UpdateRuleset function does not marshal ByPassActor if passed as an empty array. -// -// GitHub API docs: https://docs.github.com/rest/repos/rules#update-a-repository-ruleset -// -//meta:operation PUT /repos/{owner}/{repo}/rulesets/{ruleset_id} -func (s *RepositoriesService) UpdateRulesetClearBypassActor(ctx context.Context, owner, repo string, rulesetID int64) (*Response, error) { - u := fmt.Sprintf("repos/%v/%v/rulesets/%v", owner, repo, rulesetID) - - rsClearBypassActor := rulesetClearBypassActors{ - BypassActors: []*BypassActor{}, - } - - req, err := s.client.NewRequest("PUT", u, rsClearBypassActor) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(ctx, req, nil) - if err != nil { - return resp, err - } - - return resp, nil -} - -// UpdateRulesetNoBypassActor updates a repository ruleset for the specified repository. -// -// This function is necessary as the UpdateRuleset function does not marshal ByPassActor if passed as an empty array. -// -// Deprecated: Use UpdateRulesetClearBypassActor instead. -// -// GitHub API docs: https://docs.github.com/rest/repos/rules#update-a-repository-ruleset -// -//meta:operation PUT /repos/{owner}/{repo}/rulesets/{ruleset_id} -func (s *RepositoriesService) UpdateRulesetNoBypassActor(ctx context.Context, owner, repo string, rulesetID int64, ruleset RepositoryRuleset) (*RepositoryRuleset, *Response, error) { - u := fmt.Sprintf("repos/%v/%v/rulesets/%v", owner, repo, rulesetID) - - rsNoBypassActor := rulesetNoOmitBypassActors{ - ID: ruleset.ID, - Name: ruleset.Name, - Target: ruleset.Target, - SourceType: ruleset.SourceType, - Source: ruleset.Source, - Enforcement: ruleset.Enforcement, - BypassActors: ruleset.BypassActors, - NodeID: ruleset.NodeID, - Links: ruleset.Links, - Conditions: ruleset.Conditions, - Rules: ruleset.Rules, - } - - req, err := s.client.NewRequest("PUT", u, rsNoBypassActor) - if err != nil { - return nil, nil, err - } - - var rs *RepositoryRuleset - resp, err := s.client.Do(ctx, req, &rs) - if err != nil { - return nil, resp, err - } - - return rs, resp, nil -} - // DeleteRuleset deletes a repository ruleset for the specified repository. // // GitHub API docs: https://docs.github.com/rest/repos/rules#delete-a-repository-ruleset diff --git a/github/repos_rules_test.go b/github/repos_rules_test.go index 4b93d0bf14e..c8cd936e275 100644 --- a/github/repos_rules_test.go +++ b/github/repos_rules_test.go @@ -6,6 +6,7 @@ package github import ( + "encoding/json" "fmt" "net/http" "testing" @@ -63,6 +64,75 @@ func TestRepositoriesService_GetRulesForBranch(t *testing.T) { }) } +func TestRepositoriesService_UpdateRuleset_OmitZero_Nil(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + var v map[string]any + if err := json.NewDecoder(r.Body).Decode(&v); err != nil { + t.Errorf("could not decode body: %v", err) + } + + if _, ok := v["bypass_actors"]; ok { + t.Error("Request body contained 'bypass_actors', expected it to be omitted for nil input") + } + + fmt.Fprint(w, `{ + "id": 42, + "name": "ruleset", + "source": "o/repo", + "enforcement": "active" + }`) + }) + + ctx := t.Context() + input := RepositoryRuleset{ + Name: "ruleset", + Enforcement: RulesetEnforcementActive, + BypassActors: nil, + } + + _, _, err := client.Repositories.UpdateRuleset(ctx, "o", "repo", 42, input) + if err != nil { + t.Errorf("Repositories.UpdateRuleset returned error: %v", err) + } +} + +func TestRepositoriesService_UpdateRuleset_OmitZero_EmptySlice(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // Scenario 2: User passes empty slice (non-zero value). + mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + + testBody(t, r, `{"name":"ruleset","source":"","enforcement":"active","bypass_actors":[]}`+"\n") + + fmt.Fprint(w, `{ + "id": 42, + "name": "ruleset", + "source": "o/repo", + "enforcement": "active", + "bypass_actors": [] + }`) + }) + + ctx := t.Context() + input := RepositoryRuleset{ + Name: "ruleset", + Enforcement: RulesetEnforcementActive, + BypassActors: []*BypassActor{}, + } + + _, _, err := client.Repositories.UpdateRuleset(ctx, "o", "repo", 42, input) + if err != nil { + t.Errorf("Repositories.UpdateRuleset returned error: %v", err) + } +} + func TestRepositoriesService_GetRulesForBranch_ListOptions(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -454,103 +524,6 @@ func TestRepositoriesService_UpdateRuleset(t *testing.T) { }) } -func TestRepositoriesService_UpdateRulesetClearBypassActor(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "PUT") - testBody(t, r, `{"bypass_actors":[]}`+"\n") - fmt.Fprint(w, `{ - "id": 42, - "name": "ruleset", - "source_type": "Repository", - "source": "o/repo", - "enforcement": "active" - "conditions": { - "ref_name": { - "include": [ - "refs/heads/main", - "refs/heads/master" - ], - "exclude": [ - "refs/heads/dev*" - ] - } - }, - "rules": [ - { - "type": "creation" - } - ] - }`) - }) - - ctx := t.Context() - - _, err := client.Repositories.UpdateRulesetClearBypassActor(ctx, "o", "repo", 42) - if err != nil { - t.Errorf("Repositories.UpdateRulesetClearBypassActor returned error: %v \n", err) - } - - const methodName = "UpdateRulesetClearBypassActor" - - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - return client.Repositories.UpdateRulesetClearBypassActor(ctx, "o", "repo", 42) - }) -} - -func TestRepositoriesService_UpdateRulesetNoBypassActor(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - rs := RepositoryRuleset{ - Name: "ruleset", - Source: "o/repo", - Enforcement: RulesetEnforcementActive, - } - - mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "PUT") - fmt.Fprint(w, `{ - "id": 42, - "name": "ruleset", - "source_type": "Repository", - "source": "o/repo", - "enforcement": "active" - }`) - }) - - ctx := t.Context() - - ruleSet, _, err := client.Repositories.UpdateRulesetNoBypassActor(ctx, "o", "repo", 42, rs) - if err != nil { - t.Errorf("Repositories.UpdateRulesetNoBypassActor returned error: %v \n", err) - } - - want := &RepositoryRuleset{ - ID: Ptr(int64(42)), - Name: "ruleset", - SourceType: Ptr(RulesetSourceTypeRepository), - Source: "o/repo", - Enforcement: RulesetEnforcementActive, - } - - if !cmp.Equal(ruleSet, want) { - t.Errorf("Repositories.UpdateRulesetNoBypassActor returned %+v, want %+v", ruleSet, want) - } - - const methodName = "UpdateRulesetNoBypassActor" - - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Repositories.UpdateRulesetNoBypassActor(ctx, "o", "repo", 42, RepositoryRuleset{}) - if got != nil { - t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) - } - return resp, err - }) -} - func TestRepositoriesService_DeleteRuleset(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/rules.go b/github/rules.go index 420f4feb509..99b4c3aa2a9 100644 --- a/github/rules.go +++ b/github/rules.go @@ -192,7 +192,7 @@ type RepositoryRuleset struct { SourceType *RulesetSourceType `json:"source_type,omitempty"` Source string `json:"source"` Enforcement RulesetEnforcement `json:"enforcement"` - BypassActors []*BypassActor `json:"bypass_actors,omitempty"` + BypassActors []*BypassActor `json:"bypass_actors,omitzero"` CurrentUserCanBypass *BypassMode `json:"current_user_can_bypass,omitempty"` NodeID *string `json:"node_id,omitempty"` Links *RepositoryRulesetLinks `json:"_links,omitempty"` From e9632eeada4599ab1858d7e999a9bf23db14d41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irving=20Mondrag=C3=B3n?= Date: Thu, 25 Dec 2025 13:32:32 +0100 Subject: [PATCH 48/49] feat: Add support for URL custom property value type (#3879) --- ...enterprise_organization_properties_test.go | 4 +-- github/enterprise_properties_test.go | 20 +++++------ github/event_types_test.go | 2 +- github/orgs_properties.go | 11 +++++- github/orgs_properties_test.go | 34 +++++++++++++------ 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/github/enterprise_organization_properties_test.go b/github/enterprise_organization_properties_test.go index 2248285bd3f..567676eafb3 100644 --- a/github/enterprise_organization_properties_test.go +++ b/github/enterprise_organization_properties_test.go @@ -38,7 +38,7 @@ func TestEnterpriseService_GetOrganizationCustomPropertySchema(t *testing.T) { Properties: []*CustomProperty{ { PropertyName: Ptr("team"), - ValueType: "string", + ValueType: PropertyValueTypeString, Description: Ptr("Team name"), }, }, @@ -111,7 +111,7 @@ func TestEnterpriseService_GetOrganizationCustomProperty(t *testing.T) { want := &CustomProperty{ PropertyName: Ptr("team"), - ValueType: "string", + ValueType: PropertyValueTypeString, Description: Ptr("Team name"), } diff --git a/github/enterprise_properties_test.go b/github/enterprise_properties_test.go index 5aa524f4ed2..94bf1db966c 100644 --- a/github/enterprise_properties_test.go +++ b/github/enterprise_properties_test.go @@ -53,7 +53,7 @@ func TestEnterpriseService_GetAllCustomProperties(t *testing.T) { want := []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -62,11 +62,11 @@ func TestEnterpriseService_GetAllCustomProperties(t *testing.T) { }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, { PropertyName: Ptr("team"), - ValueType: "string", + ValueType: PropertyValueTypeString, Description: Ptr("Team owning the repository"), }, } @@ -109,12 +109,12 @@ func TestEnterpriseService_CreateOrUpdateCustomProperties(t *testing.T) { properties, _, err := client.Enterprise.CreateOrUpdateCustomProperties(ctx, "e", []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, }) if err != nil { @@ -124,12 +124,12 @@ func TestEnterpriseService_CreateOrUpdateCustomProperties(t *testing.T) { want := []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, } @@ -176,7 +176,7 @@ func TestEnterpriseService_GetCustomProperty(t *testing.T) { want := &CustomProperty{ PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -220,7 +220,7 @@ func TestEnterpriseService_CreateOrUpdateCustomProperty(t *testing.T) { ctx := t.Context() property, _, err := client.Enterprise.CreateOrUpdateCustomProperty(ctx, "e", "name", &CustomProperty{ - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -233,7 +233,7 @@ func TestEnterpriseService_CreateOrUpdateCustomProperty(t *testing.T) { want := &CustomProperty{ PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), diff --git a/github/event_types_test.go b/github/event_types_test.go index 955de1b09e0..05843206d49 100644 --- a/github/event_types_test.go +++ b/github/event_types_test.go @@ -13729,7 +13729,7 @@ func TestCustomPropertyEvent_Marshal(t *testing.T) { Action: Ptr("created"), Definition: &CustomProperty{ PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, SourceType: Ptr("enterprise"), Required: Ptr(true), DefaultValue: Ptr("production"), diff --git a/github/orgs_properties.go b/github/orgs_properties.go index 0c23c91b227..502713b8bac 100644 --- a/github/orgs_properties.go +++ b/github/orgs_properties.go @@ -12,6 +12,15 @@ import ( "fmt" ) +// Valid values for CustomProperty.ValueType. +const ( + PropertyValueTypeString = "string" + PropertyValueTypeSingleSelect = "single_select" + PropertyValueTypeMultiSelect = "multi_select" + PropertyValueTypeTrueFalse = "true_false" + PropertyValueTypeURL = "url" +) + // CustomProperty represents an organization custom property object. type CustomProperty struct { // PropertyName is required for most endpoints except when calling CreateOrUpdateCustomProperty; @@ -21,7 +30,7 @@ type CustomProperty struct { URL *string `json:"url,omitempty"` // SourceType is the source type of the property where it has been created. Can be one of: organization, enterprise. SourceType *string `json:"source_type,omitempty"` - // The type of the value for the property. Can be one of: string, single_select, multi_select, true_false. + // The type of the value for the property. Can be one of: string, single_select, multi_select, true_false, url. ValueType string `json:"value_type"` // Whether the property is required. Required *bool `json:"required,omitempty"` diff --git a/github/orgs_properties_test.go b/github/orgs_properties_test.go index 7ff20fd34c7..bac673d3eed 100644 --- a/github/orgs_properties_test.go +++ b/github/orgs_properties_test.go @@ -40,6 +40,13 @@ func TestOrganizationsService_GetAllCustomProperties(t *testing.T) { "property_name": "team", "value_type": "string", "description": "Team owning the repository" + }, + { + "property_name": "documentation", + "value_type": "url", + "required": true, + "description": "Link to the documentation", + "default_value": "https://example.com/docs" } ]`) }) @@ -53,7 +60,7 @@ func TestOrganizationsService_GetAllCustomProperties(t *testing.T) { want := []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -62,13 +69,20 @@ func TestOrganizationsService_GetAllCustomProperties(t *testing.T) { }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, { PropertyName: Ptr("team"), - ValueType: "string", + ValueType: PropertyValueTypeString, Description: Ptr("Team owning the repository"), }, + { + PropertyName: Ptr("documentation"), + ValueType: PropertyValueTypeURL, + Required: Ptr(true), + Description: Ptr("Link to the documentation"), + DefaultValue: Ptr("https://example.com/docs"), + }, } if !cmp.Equal(properties, want) { t.Errorf("Organizations.GetAllCustomProperties returned %+v, want %+v", properties, want) @@ -109,12 +123,12 @@ func TestOrganizationsService_CreateOrUpdateCustomProperties(t *testing.T) { properties, _, err := client.Organizations.CreateOrUpdateCustomProperties(ctx, "o", []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, }) if err != nil { @@ -124,12 +138,12 @@ func TestOrganizationsService_CreateOrUpdateCustomProperties(t *testing.T) { want := []*CustomProperty{ { PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), }, { PropertyName: Ptr("service"), - ValueType: "string", + ValueType: PropertyValueTypeString, }, } @@ -176,7 +190,7 @@ func TestOrganizationsService_GetCustomProperty(t *testing.T) { want := &CustomProperty{ PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -220,7 +234,7 @@ func TestOrganizationsService_CreateOrUpdateCustomProperty(t *testing.T) { ctx := t.Context() property, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, "o", "name", &CustomProperty{ - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), @@ -233,7 +247,7 @@ func TestOrganizationsService_CreateOrUpdateCustomProperty(t *testing.T) { want := &CustomProperty{ PropertyName: Ptr("name"), - ValueType: "single_select", + ValueType: PropertyValueTypeSingleSelect, Required: Ptr(true), DefaultValue: Ptr("production"), Description: Ptr("Prod or dev environment"), From 91881544a9e64e14ad8f9223755a1e3ad91a2841 Mon Sep 17 00:00:00 2001 From: RAKESH S <124438543+rockygeekz@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:08:49 +0530 Subject: [PATCH 49/49] feat: Add UploadReleaseAssetFromRelease convenience helper (#3851) --- example/uploadreleaseassetfromrelease/main.go | 61 ++++++ github/repos_releases.go | 73 +++++++ github/repos_releases_test.go | 203 ++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 example/uploadreleaseassetfromrelease/main.go diff --git a/example/uploadreleaseassetfromrelease/main.go b/example/uploadreleaseassetfromrelease/main.go new file mode 100644 index 00000000000..78fc07dc7cb --- /dev/null +++ b/example/uploadreleaseassetfromrelease/main.go @@ -0,0 +1,61 @@ +// 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. + +// The uploadreleaseassetfromrelease example demonstrates how to upload +// a release asset using the UploadReleaseAssetFromRelease helper. +package main + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + + "github.com/google/go-github/v80/github" +) + +func main() { + token := os.Getenv("GITHUB_AUTH_TOKEN") + if token == "" { + log.Fatal("GITHUB_AUTH_TOKEN not set") + } + + ctx := context.Background() + client := github.NewClient(nil).WithAuthToken(token) + + owner := "OWNER" + repo := "REPO" + releaseID := int64(1) + + // Fetch the release (UploadURL is populated by the API) + release, _, err := client.Repositories.GetRelease(ctx, owner, repo, releaseID) + if err != nil { + log.Fatalf("GetRelease failed: %v", err) + } + + // Asset content + data := []byte("Hello from go-github!\n") + reader := bytes.NewReader(data) + size := int64(len(data)) + + opts := &github.UploadOptions{ + Name: "example.txt", + Label: "Example asset", + } + + asset, _, err := client.Repositories.UploadReleaseAssetFromRelease( + ctx, + release, + opts, + reader, + size, + ) + if err != nil { + log.Fatalf("UploadReleaseAssetFromRelease failed: %v", err) + } + + fmt.Printf("Uploaded asset ID: %v\n", asset.GetID()) +} diff --git a/github/repos_releases.go b/github/repos_releases.go index b5cff73260b..b4668842b9a 100644 --- a/github/repos_releases.go +++ b/github/repos_releases.go @@ -488,3 +488,76 @@ func (s *RepositoriesService) UploadReleaseAsset(ctx context.Context, owner, rep } return asset, resp, nil } + +// UploadReleaseAssetFromRelease uploads an asset using the UploadURL that's embedded +// in a RepositoryRelease object. +// +// This is a convenience wrapper that extracts the release.UploadURL (which is usually +// templated like "https://uploads.github.com/.../assets{?name,label}") and uploads +// the provided data (reader + size) using the existing upload helpers. +// +// GitHub API docs: https://docs.github.com/rest/releases/assets#upload-a-release-asset +// +//meta:operation POST /repos/{owner}/{repo}/releases/{release_id}/assets +func (s *RepositoriesService) UploadReleaseAssetFromRelease( + ctx context.Context, + release *RepositoryRelease, + opts *UploadOptions, + reader io.Reader, + size int64, +) (*ReleaseAsset, *Response, error) { + if release == nil || release.UploadURL == nil { + return nil, nil, errors.New("release UploadURL must be provided") + } + if reader == nil { + return nil, nil, errors.New("reader must be provided") + } + if size < 0 { + return nil, nil, errors.New("size must be >= 0") + } + + // Strip URI-template portion (e.g. "{?name,label}") if present. + uploadURL := *release.UploadURL + if idx := strings.Index(uploadURL, "{"); idx != -1 { + uploadURL = uploadURL[:idx] + } + + // If this is a *relative* URL (no scheme), normalize it by trimming a leading "/" + // so it works with Client.BaseURL path prefixes (e.g. "/api-v3/"). + if !strings.HasPrefix(uploadURL, "http://") && !strings.HasPrefix(uploadURL, "https://") { + uploadURL = strings.TrimPrefix(uploadURL, "/") + } + + // addOptions will append name/label query params (same behavior as UploadReleaseAsset). + u, err := addOptions(uploadURL, opts) + if err != nil { + return nil, nil, err + } + + // determine media type + mediaType := defaultMediaType + if opts != nil { + switch { + case opts.MediaType != "": + mediaType = opts.MediaType + case opts.Name != "": + if ext := filepath.Ext(opts.Name); ext != "" { + if mt := mime.TypeByExtension(ext); mt != "" { + mediaType = mt + } + } + } + } + + req, err := s.client.NewUploadRequest(u, reader, size, mediaType) + if err != nil { + return nil, nil, err + } + + asset := new(ReleaseAsset) + resp, err := s.client.Do(ctx, req, asset) + if err != nil { + return nil, resp, err + } + return asset, resp, nil +} diff --git a/github/repos_releases_test.go b/github/repos_releases_test.go index 6d2d5fd265d..b161c0d7a5c 100644 --- a/github/repos_releases_test.go +++ b/github/repos_releases_test.go @@ -930,3 +930,206 @@ func TestGenerateNotesOptions_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } + +func TestRepositoriesService_UploadReleaseAssetFromRelease(t *testing.T) { + t.Parallel() + + var ( + defaultUploadOptions = &UploadOptions{Name: "n.txt"} + defaultExpectedFormValue = values{"name": "n.txt"} + mediaTypeTextPlain = "text/plain; charset=utf-8" + ) + + client, mux, _ := setup(t) + + // Use the same endpoint path used in other release asset tests. + mux.HandleFunc("/repos/o/r/releases/1/assets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", mediaTypeTextPlain) + testHeader(t, r, "Content-Length", "12") + testFormValues(t, r, defaultExpectedFormValue) + testBody(t, r, "Upload me !\n") + + fmt.Fprint(w, `{"id":1}`) + }) + + body := []byte("Upload me !\n") + reader := bytes.NewReader(body) + size := int64(len(body)) + + // Provide a templated upload URL like GitHub returns. + uploadURL := "/repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{ + UploadURL: &uploadURL, + } + + ctx := t.Context() + asset, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, defaultUploadOptions, reader, size) + if err != nil { + t.Fatalf("Repositories.UploadReleaseAssetFromRelease returned error: %v", err) + } + want := &ReleaseAsset{ID: Ptr(int64(1))} + if !cmp.Equal(asset, want) { + t.Fatalf("Repositories.UploadReleaseAssetFromRelease returned %+v, want %+v", asset, want) + } +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_AbsoluteTemplate(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/releases/1/assets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + // Expect name query param created by addOptions after trimming template. + if got := r.URL.Query().Get("name"); got != "abs.txt" { + t.Errorf("Expected name query param 'abs.txt', got %q", got) + } + fmt.Fprint(w, `{"id":1}`) + }) + + body := []byte("Upload me !\n") + reader := bytes.NewReader(body) + size := int64(len(body)) + + // Build an absolute URL using the test client's BaseURL. + absoluteUploadURL := client.BaseURL.String() + "repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{UploadURL: &absoluteUploadURL} + + opts := &UploadOptions{Name: "abs.txt"} + ctx := t.Context() + asset, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, opts, reader, size) + if err != nil { + t.Fatalf("UploadReleaseAssetFromRelease returned error: %v", err) + } + want := &ReleaseAsset{ID: Ptr(int64(1))} + if !cmp.Equal(asset, want) { + t.Fatalf("UploadReleaseAssetFromRelease returned %+v, want %+v", asset, want) + } +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_NilRelease(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + body := []byte("Upload me !\n") + reader := bytes.NewReader(body) + size := int64(len(body)) + + ctx := t.Context() + _, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, nil, &UploadOptions{Name: "n.txt"}, reader, size) + if err == nil { + t.Fatal("expected error for nil release, got nil") + } + + const methodName = "UploadReleaseAssetFromRelease" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Repositories.UploadReleaseAssetFromRelease(ctx, nil, &UploadOptions{Name: "n.txt"}, reader, size) + return err + }) +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_NilReader(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + uploadURL := "/repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{UploadURL: &uploadURL} + + ctx := t.Context() + _, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, &UploadOptions{Name: "n.txt"}, nil, 12) + if err == nil { + t.Fatal("expected error when reader is nil") + } + + const methodName = "UploadReleaseAssetFromRelease" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Repositories.UploadReleaseAssetFromRelease(ctx, release, &UploadOptions{Name: "n.txt"}, nil, 12) + return err + }) +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_NegativeSize(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + uploadURL := "/repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{UploadURL: &uploadURL} + + body := []byte("Upload me !\n") + reader := bytes.NewReader(body) + + ctx := t.Context() + _, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, &UploadOptions{Name: "n..txt"}, reader, -1) + if err == nil { + t.Fatal("expected error when size is negative") + } +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_NoOpts(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // No opts: we just assert that the handler is hit and body is as expected. + mux.HandleFunc("/repos/o/r/releases/1/assets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, "Upload me !\n") + fmt.Fprint(w, `{"id":1}`) + }) + + body := []byte("Upload me !\n") + reader := bytes.NewReader(body) + size := int64(len(body)) + + uploadURL := "/repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{UploadURL: &uploadURL} + + ctx := t.Context() + asset, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, nil, reader, size) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := &ReleaseAsset{ID: Ptr(int64(1))} + if !cmp.Equal(asset, want) { + t.Fatalf("Repositories.UploadReleaseAssetFromRelease returned %+v, want %+v", asset, want) + } + + const methodName = "UploadReleaseAssetFromRelease" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, nil, reader, size) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestRepositoriesService_UploadReleaseAssetFromRelease_WithMediaType(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // Expect explicit media type to be used. + mux.HandleFunc("/repos/o/r/releases/1/assets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", "image/png") + fmt.Fprint(w, `{"id":1}`) + }) + + body := []byte("Binary!") + reader := bytes.NewReader(body) + size := int64(len(body)) + + uploadURL := "/repos/o/r/releases/1/assets{?name,label}" + release := &RepositoryRelease{UploadURL: &uploadURL} + + opts := &UploadOptions{Name: "n.txt", MediaType: "image/png"} + + ctx := t.Context() + asset, _, err := client.Repositories.UploadReleaseAssetFromRelease(ctx, release, opts, reader, size) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := &ReleaseAsset{ID: Ptr(int64(1))} + if !cmp.Equal(asset, want) { + t.Fatalf("UploadReleaseAssetFromRelease returned %+v, want %+v", asset, want) + } +}