diff --git a/go.mod b/go.mod index e5ee41161..67604a4f0 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/go-sql-driver/mysql v1.9.3 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.0 - github.com/google/go-github/v74 v74.0.0 + github.com/google/go-github/v75 v75.0.0 github.com/google/tink/go v1.7.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 @@ -39,6 +39,7 @@ require ( github.com/kinbiko/jsonassert v1.2.0 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.32 + github.com/migueleliasweb/go-github-mock v1.4.0 github.com/moby/term v0.5.2 github.com/muesli/termenv v0.16.0 github.com/neticdk/go-bitbucket v1.0.4 @@ -130,8 +131,10 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-github/v73 v73.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect @@ -187,12 +190,13 @@ require ( github.com/syndtr/goleveldb v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - github.com/urfave/cli/v2 v2.3.0 // indirect + github.com/urfave/cli/v2 v2.25.3 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect @@ -212,7 +216,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gotest.tools/v3 v3.4.0 // indirect + gotest.tools/v3 v3.5.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/go.sum b/go.sum index b7f500869..a7515ddea 100644 --- a/go.sum +++ b/go.sum @@ -103,7 +103,6 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -249,12 +248,13 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/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/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= -github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= +github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= +github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= +github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= +github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -269,6 +269,8 @@ github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcg github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -406,6 +408,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/migueleliasweb/go-github-mock v1.4.0 h1:pQ6K8r348m2q79A8Khb0PbEeNQV7t3h1xgECV+jNpXk= +github.com/migueleliasweb/go-github-mock v1.4.0/go.mod h1:/DUmhXkxrgVlDOVBqGoUXkV4w0ms5n1jDQHotYm135o= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -502,7 +506,6 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -513,7 +516,6 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -554,8 +556,8 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= +github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= @@ -571,6 +573,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yaronf/httpsign v0.3.2 h1:rBYYx9eHm60noI4oC24JAD2tJuM8AUDh9ErJO/FfzBs= github.com/yaronf/httpsign v0.3.2/go.mod h1:cYB/6toJrJnf4JTLVoo6IHzFH9/Zu1dcKmah4xfX2WA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -694,7 +698,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -742,7 +745,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= @@ -783,7 +785,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -792,8 +793,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= diff --git a/server/forge/github/convert.go b/server/forge/github/convert.go index bda0a213f..8f3fbe96d 100644 --- a/server/forge/github/convert.go +++ b/server/forge/github/convert.go @@ -18,7 +18,7 @@ package github import ( "fmt" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v75/github" "go.woodpecker-ci.org/woodpecker/v3/server/model" ) diff --git a/server/forge/github/convert_test.go b/server/forge/github/convert_test.go index e90b0df3e..33b031846 100644 --- a/server/forge/github/convert_test.go +++ b/server/forge/github/convert_test.go @@ -18,16 +18,12 @@ package github import ( "testing" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v75/github" "github.com/stretchr/testify/assert" "go.woodpecker-ci.org/woodpecker/v3/server/model" ) -const ( - stateOpen = "open" -) - func Test_convertStatus(t *testing.T) { assert.Equal(t, statusSuccess, convertStatus(model.StatusSuccess)) assert.Equal(t, statusPending, convertStatus(model.StatusPending)) @@ -162,121 +158,3 @@ func Test_convertRepoHook(t *testing.T) { assert.Equal(t, *from.DefaultBranch, repo.Branch) }) } - -func Test_parsePullHook(t *testing.T) { - from := &github.PullRequestEvent{ - Action: github.Ptr(actionOpen), - PullRequest: &github.PullRequest{ - State: github.Ptr(stateOpen), - HTMLURL: github.Ptr("https://github.com/octocat/hello-world/pulls/42"), - Number: github.Ptr(42), - Title: github.Ptr("Updated README.md"), - Base: &github.PullRequestBranch{ - Ref: github.Ptr("main"), - }, - Head: &github.PullRequestBranch{ - Ref: github.Ptr("changes"), - SHA: github.Ptr("f72fc19"), - Repo: &github.Repository{ - CloneURL: github.Ptr("https://github.com/octocat/hello-world-fork"), - }, - }, - User: &github.User{ - Login: github.Ptr("octocat"), - AvatarURL: github.Ptr("https://avatars1.githubusercontent.com/u/583231"), - }, - }, Sender: &github.User{ - Login: github.Ptr("octocat"), - }, - } - pull, _, pipeline, err := parsePullHook(from, true) - assert.NoError(t, err) - assert.NotNil(t, pull) - assert.Equal(t, model.EventPull, pipeline.Event) - assert.Equal(t, *from.PullRequest.Base.Ref, pipeline.Branch) - assert.Equal(t, "refs/pull/42/merge", pipeline.Ref) - assert.Equal(t, "changes:main", pipeline.Refspec) - assert.Equal(t, *from.PullRequest.Head.SHA, pipeline.Commit) - assert.Equal(t, *from.PullRequest.Title, pipeline.Message) - assert.Equal(t, *from.PullRequest.Title, pipeline.Title) - assert.Equal(t, *from.PullRequest.User.Login, pipeline.Author) - assert.Equal(t, *from.PullRequest.User.AvatarURL, pipeline.Avatar) - assert.Equal(t, *from.Sender.Login, pipeline.Sender) -} - -func Test_parseDeployHook(t *testing.T) { - from := &github.DeploymentEvent{Deployment: &github.Deployment{}, Sender: &github.User{}} - from.Deployment.Description = github.Ptr(":shipit:") - from.Deployment.Environment = github.Ptr("production") - from.Deployment.Task = github.Ptr("deploy") - from.Deployment.ID = github.Ptr(int64(42)) - from.Deployment.Ref = github.Ptr("main") - from.Deployment.SHA = github.Ptr("f72fc19") - from.Deployment.URL = github.Ptr("https://github.com/octocat/hello-world") - from.Sender.Login = github.Ptr("octocat") - from.Sender.AvatarURL = github.Ptr("https://avatars1.githubusercontent.com/u/583231") - - _, pipeline := parseDeployHook(from) - assert.Equal(t, model.EventDeploy, pipeline.Event) - assert.Equal(t, "main", pipeline.Branch) - assert.Equal(t, "refs/heads/main", pipeline.Ref) - assert.Equal(t, *from.Deployment.SHA, pipeline.Commit) - assert.Equal(t, *from.Deployment.Description, pipeline.Message) - assert.Equal(t, *from.Deployment.URL, pipeline.ForgeURL) - assert.Equal(t, *from.Sender.Login, pipeline.Author) - assert.Equal(t, *from.Sender.AvatarURL, pipeline.Avatar) -} - -func Test_parsePushHook(t *testing.T) { - t.Run("convert push from webhook", func(t *testing.T) { - from := &github.PushEvent{Sender: &github.User{}, Repo: &github.PushEventRepository{}, HeadCommit: &github.HeadCommit{Author: &github.CommitAuthor{}}} - from.Sender.Login = github.Ptr("octocat") - from.Sender.AvatarURL = github.Ptr("https://avatars1.githubusercontent.com/u/583231") - from.Repo.CloneURL = github.Ptr("https://github.com/octocat/hello-world.git") - from.HeadCommit.Author.Email = github.Ptr("github.Ptr(octocat@github.com") - from.HeadCommit.Message = github.Ptr("updated README.md") - from.HeadCommit.URL = github.Ptr("https://github.com/octocat/hello-world") - from.HeadCommit.ID = github.Ptr("f72fc19") - from.Ref = github.Ptr("refs/heads/main") - - _, pipeline := parsePushHook(from) - assert.Equal(t, model.EventPush, pipeline.Event) - assert.Equal(t, "main", pipeline.Branch) - assert.Equal(t, "refs/heads/main", pipeline.Ref) - assert.Equal(t, *from.HeadCommit.ID, pipeline.Commit) - assert.Equal(t, *from.HeadCommit.Message, pipeline.Message) - assert.Equal(t, *from.HeadCommit.URL, pipeline.ForgeURL) - assert.Equal(t, *from.Sender.Login, pipeline.Author) - assert.Equal(t, *from.Sender.AvatarURL, pipeline.Avatar) - assert.Equal(t, *from.HeadCommit.Author.Email, pipeline.Email) - }) - - t.Run("convert tag from webhook", func(t *testing.T) { - from := &github.PushEvent{} - from.Ref = github.Ptr("refs/tags/v1.0.0") - - _, pipeline := parsePushHook(from) - assert.Equal(t, model.EventTag, pipeline.Event) - assert.Equal(t, "refs/tags/v1.0.0", pipeline.Ref) - }) - - t.Run("convert tag's base branch to pipeline's branch ", func(t *testing.T) { - from := &github.PushEvent{} - from.Ref = github.Ptr("refs/tags/v1.0.0") - from.BaseRef = github.Ptr("refs/heads/main") - - _, pipeline := parsePushHook(from) - assert.Equal(t, model.EventTag, pipeline.Event) - assert.Equal(t, "main", pipeline.Branch) - }) - - t.Run("not convert tag's base_ref from webhook if not prefixed with 'ref/heads/'", func(t *testing.T) { - from := &github.PushEvent{} - from.Ref = github.Ptr("refs/tags/v1.0.0") - from.BaseRef = github.Ptr("refs/refs/main") - - _, pipeline := parsePushHook(from) - assert.Equal(t, model.EventTag, pipeline.Event) - assert.Equal(t, "refs/tags/v1.0.0", pipeline.Branch) - }) -} diff --git a/server/forge/github/fixtures/HookTag.json b/server/forge/github/fixtures/HookTag.json new file mode 100644 index 000000000..a3e2f1e20 --- /dev/null +++ b/server/forge/github/fixtures/HookTag.json @@ -0,0 +1,162 @@ +{ + "ref": "refs/tags/the-tag-v1", + "before": "0000000000000000000000000000000000000000", + "after": "67012991d6c69b1c58378346fca366b864d8d1a1", + "repository": { + "id": 1028609128, + "node_id": "R_kgDOPU9UaA", + "name": "test_ci_tmp", + "full_name": "6543/test_ci_tmp", + "private": false, + "owner": { + "name": "6543", + "email": "6543@obermui.de", + "login": "6543", + "id": 24977596, + "node_id": "MDQ6VXNlcjI0OTc3NTk2", + "avatar_url": "https://avatars.githubusercontent.com/u/24977596?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/6543", + "html_url": "https://github.com/6543", + "followers_url": "https://api.github.com/users/6543/followers", + "following_url": "https://api.github.com/users/6543/following{/other_user}", + "gists_url": "https://api.github.com/users/6543/gists{/gist_id}", + "starred_url": "https://api.github.com/users/6543/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/6543/subscriptions", + "organizations_url": "https://api.github.com/users/6543/orgs", + "repos_url": "https://api.github.com/users/6543/repos", + "events_url": "https://api.github.com/users/6543/events{/privacy}", + "received_events_url": "https://api.github.com/users/6543/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/6543/test_ci_tmp", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/6543/test_ci_tmp", + "forks_url": "https://api.github.com/repos/6543/test_ci_tmp/forks", + "keys_url": "https://api.github.com/repos/6543/test_ci_tmp/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/6543/test_ci_tmp/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/6543/test_ci_tmp/teams", + "hooks_url": "https://api.github.com/repos/6543/test_ci_tmp/hooks", + "issue_events_url": "https://api.github.com/repos/6543/test_ci_tmp/issues/events{/number}", + "events_url": "https://api.github.com/repos/6543/test_ci_tmp/events", + "assignees_url": "https://api.github.com/repos/6543/test_ci_tmp/assignees{/user}", + "branches_url": "https://api.github.com/repos/6543/test_ci_tmp/branches{/branch}", + "tags_url": "https://api.github.com/repos/6543/test_ci_tmp/tags", + "blobs_url": "https://api.github.com/repos/6543/test_ci_tmp/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/6543/test_ci_tmp/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/6543/test_ci_tmp/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/6543/test_ci_tmp/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/6543/test_ci_tmp/statuses/{sha}", + "languages_url": "https://api.github.com/repos/6543/test_ci_tmp/languages", + "stargazers_url": "https://api.github.com/repos/6543/test_ci_tmp/stargazers", + "contributors_url": "https://api.github.com/repos/6543/test_ci_tmp/contributors", + "subscribers_url": "https://api.github.com/repos/6543/test_ci_tmp/subscribers", + "subscription_url": "https://api.github.com/repos/6543/test_ci_tmp/subscription", + "commits_url": "https://api.github.com/repos/6543/test_ci_tmp/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/6543/test_ci_tmp/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/6543/test_ci_tmp/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/6543/test_ci_tmp/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/6543/test_ci_tmp/contents/{+path}", + "compare_url": "https://api.github.com/repos/6543/test_ci_tmp/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/6543/test_ci_tmp/merges", + "archive_url": "https://api.github.com/repos/6543/test_ci_tmp/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/6543/test_ci_tmp/downloads", + "issues_url": "https://api.github.com/repos/6543/test_ci_tmp/issues{/number}", + "pulls_url": "https://api.github.com/repos/6543/test_ci_tmp/pulls{/number}", + "milestones_url": "https://api.github.com/repos/6543/test_ci_tmp/milestones{/number}", + "notifications_url": "https://api.github.com/repos/6543/test_ci_tmp/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/6543/test_ci_tmp/labels{/name}", + "releases_url": "https://api.github.com/repos/6543/test_ci_tmp/releases{/id}", + "deployments_url": "https://api.github.com/repos/6543/test_ci_tmp/deployments", + "created_at": 1753817741, + "updated_at": "2025-07-29T19:36:23Z", + "pushed_at": 1760097372, + "git_url": "git://github.com/6543/test_ci_tmp.git", + "ssh_url": "git@github.com:6543/test_ci_tmp.git", + "clone_url": "https://github.com/6543/test_ci_tmp.git", + "svn_url": "https://github.com/6543/test_ci_tmp", + "homepage": null, + "size": 3, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Dockerfile", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main", + "stargazers": 0, + "master_branch": "main" + }, + "pusher": { + "name": "6543", + "email": "6543@obermui.de" + }, + "sender": { + "login": "6543", + "id": 24977596, + "node_id": "MDQ6VXNlcjI0OTc3NTk2", + "avatar_url": "https://avatars.githubusercontent.com/u/24977596?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/6543", + "html_url": "https://github.com/6543", + "followers_url": "https://api.github.com/users/6543/followers", + "following_url": "https://api.github.com/users/6543/following{/other_user}", + "gists_url": "https://api.github.com/users/6543/gists{/gist_id}", + "starred_url": "https://api.github.com/users/6543/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/6543/subscriptions", + "organizations_url": "https://api.github.com/users/6543/orgs", + "repos_url": "https://api.github.com/users/6543/repos", + "events_url": "https://api.github.com/users/6543/events{/privacy}", + "received_events_url": "https://api.github.com/users/6543/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "created": true, + "deleted": false, + "forced": false, + "base_ref": "refs/heads/main", + "compare": "https://github.com/6543/test_ci_tmp/compare/the-tag-v1", + "commits": [], + "head_commit": { + "id": "67012991d6c69b1c58378346fca366b864d8d1a1", + "tree_id": "8fb363ff1374abf0b7f3598e28a15ebdc443cb02", + "distinct": true, + "message": "Update .woodpecker.yml", + "timestamp": "2025-07-29T16:41:24+02:00", + "url": "https://github.com/6543/test_ci_tmp/commit/67012991d6c69b1c58378346fca366b864d8d1a1", + "author": { + "name": "6543", + "email": "6543@obermui.de", + "username": "6543" + }, + "committer": { + "name": "6543", + "email": "6543@obermui.de", + "username": "6543" + }, + "added": [], + "removed": [], + "modified": [".woodpecker.yml"] + } +} diff --git a/server/forge/github/fixtures/hooks.go b/server/forge/github/fixtures/hooks.go index 352e82bcb..aa74ac4bc 100644 --- a/server/forge/github/fixtures/hooks.go +++ b/server/forge/github/fixtures/hooks.go @@ -77,6 +77,9 @@ var HookPullRequestEdited string //go:embed HookRelease.json var HookRelease string +//go:embed HookTag.json +var HookTag string + //go:embed HookPullRequestReviewRequested.json var HookPullRequestReviewRequested string diff --git a/server/forge/github/github.go b/server/forge/github/github.go index ccbd0343a..35fe7a83d 100644 --- a/server/forge/github/github.go +++ b/server/forge/github/github.go @@ -27,7 +27,7 @@ import ( "strings" "time" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v75/github" "github.com/rs/zerolog/log" "golang.org/x/oauth2" @@ -40,9 +40,12 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/shared/utils" ) +type contextKey string + const ( - defaultURL = "https://github.com" // Default GitHub URL - defaultAPI = "https://api.github.com/" // Default GitHub API URL + defaultURL = "https://github.com" // Default GitHub URL + defaultAPI = "https://api.github.com/" // Default GitHub API URL + githubClientKey contextKey = "github_client" ) // Opts defines configuration options. @@ -459,7 +462,13 @@ func (c *client) newConfig() *oauth2.Config { } // newClientToken returns the GitHub oauth2 client. +// It first checks if a client is available in the context, otherwise creates a new one. func (c *client) newClientToken(ctx context.Context, token string) *github.Client { + // Check if a client is already in the context + if ctxClient, ok := ctx.Value(githubClientKey).(*github.Client); ok { + return ctxClient + } + ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) @@ -606,7 +615,7 @@ func (c *client) BranchHead(ctx context.Context, u *model.User, r *model.Repo, b // Hook parses the post-commit hook from the Request body // and returns the required data in a standard format. func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) { - pull, repo, pipeline, err := parseHook(r, c.MergeRef) + pull, repo, pipeline, currCommit, prevCommit, err := parseHook(r, c.MergeRef) if err != nil { return nil, nil, err } @@ -620,11 +629,17 @@ func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model pipeline.Commit = sha } - if pull != nil && len(pipeline.ChangedFiles) == 0 { + if pull != nil { pipeline, err = c.loadChangedFilesFromPullRequest(ctx, pull, repo, pipeline) if err != nil { return nil, nil, err } + } else if pipeline.Event == model.EventPush { + // GitHub has removed commit summaries from Events API payloads from 7th October 2025 onwards. + pipeline, err = c.loadChangedFilesFromCommits(ctx, repo, pipeline, prevCommit, currCommit) + if err != nil { + return nil, nil, err + } } return repo, pipeline, nil @@ -647,24 +662,24 @@ func (c *client) loadChangedFilesFromPullRequest(ctx context.Context, pull *gith return nil, err } - pipeline.ChangedFiles, err = utils.Paginate(func(page int) ([]string, error) { - opts := &github.ListOptions{Page: page} - fileList := make([]string, 0, 16) - for opts.Page > 0 { - files, resp, err := c.newClientToken(ctx, user.AccessToken).PullRequests.ListFiles(ctx, repo.Owner, repo.Name, pull.GetNumber(), opts) - if err != nil { - return nil, err - } + gh := c.newClientToken(ctx, user.AccessToken) + fileList := make([]string, 0, 16) - for _, file := range files { - fileList = append(fileList, file.GetFilename(), file.GetPreviousFilename()) - } - - opts.Page = resp.NextPage + opts := &github.ListOptions{Page: 1} + for opts.Page > 0 { + files, resp, err := gh.PullRequests.ListFiles(ctx, repo.Owner, repo.Name, pull.GetNumber(), opts) + if err != nil { + return nil, err } - return utils.DeduplicateStrings(fileList), nil - }, -1) + for _, file := range files { + fileList = append(fileList, file.GetFilename(), file.GetPreviousFilename()) + } + + opts.Page = resp.NextPage + } + + pipeline.ChangedFiles = utils.DeduplicateStrings(fileList) return pipeline, err } @@ -710,3 +725,65 @@ func (c *client) getTagCommitSHA(ctx context.Context, repo *model.Repo, tagName } return tag.GetCommit().GetSHA(), nil } + +func (c *client) loadChangedFilesFromCommits(ctx context.Context, tmpRepo *model.Repo, pipeline *model.Pipeline, curr, prev string) (*model.Pipeline, error) { + _store, ok := store.TryFromContext(ctx) + if !ok { + log.Error().Msg("could not get store from context") + return pipeline, nil + } + + switch prev { + case curr: + log.Error().Msg("GitHub push event contains the same commit before and after, no changes detected") + return pipeline, nil + case "0000000000000000000000000000000000000000": + prev = "" + fallthrough + case "": + // For tag events, prev is empty, but we can still fetch the changed files using the current commit + log.Trace().Msg("GitHub tag event, fetching changed files using current commit") + } + + repo, err := _store.GetRepoNameFallback(tmpRepo.ForgeRemoteID, tmpRepo.FullName) + if err != nil { + return nil, err + } + + user, err := _store.GetUser(repo.UserID) + if err != nil { + return nil, err + } + + gh := c.newClientToken(ctx, user.AccessToken) + fileList := make([]string, 0, 16) + + if prev == "" { + opts := &github.ListOptions{Page: 1} + for opts.Page > 0 { + commit, resp, err := gh.Repositories.GetCommit(ctx, repo.Owner, repo.Name, curr, &github.ListOptions{}) + if err != nil { + return nil, err + } + for _, file := range commit.Files { + fileList = append(fileList, file.GetFilename(), file.GetPreviousFilename()) + } + opts.Page = resp.NextPage + } + } else { + opts := &github.ListOptions{Page: 1} + for opts.Page > 0 { + comp, resp, err := gh.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, curr, opts) + if err != nil { + return nil, err + } + for _, file := range comp.Files { + fileList = append(fileList, file.GetFilename(), file.GetPreviousFilename()) + } + opts.Page = resp.NextPage + } + } + + pipeline.ChangedFiles = utils.DeduplicateStrings(fileList) + return pipeline, err +} diff --git a/server/forge/github/github_test.go b/server/forge/github/github_test.go index 606d4ed97..6207b13e4 100644 --- a/server/forge/github/github_test.go +++ b/server/forge/github/github_test.go @@ -16,14 +16,21 @@ package github import ( + "context" "net/http/httptest" + "strings" "testing" "github.com/gin-gonic/gin" + "github.com/google/go-github/v75/github" + gh_mock "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "go.woodpecker-ci.org/woodpecker/v3/server/forge/github/fixtures" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store" + store_mocks "go.woodpecker-ci.org/woodpecker/v3/server/store/mocks" ) func TestNew(t *testing.T) { @@ -89,7 +96,7 @@ func Test_github(t *testing.T) { var ( fakeUser = &model.User{ - Login: "octocat", + Login: "6543", AccessToken: "cfcd2084", } @@ -110,3 +117,158 @@ var ( FullName: "test_name/repo_not_found", } ) + +func TestHook(t *testing.T) { + // Mock GitHub API for changed files + mockedHTTPClient := gh_mock.NewMockedHTTPClient( + gh_mock.WithRequestMatch( + gh_mock.GetReposCommitsByOwnerByRepoByRef, + github.RepositoryCommit{ + Files: []*github.CommitFile{ + {Filename: github.Ptr("README.md")}, + {Filename: github.Ptr("main.go")}, + }, + }, + ), + gh_mock.WithRequestMatch( + gh_mock.GetReposCompareByOwnerByRepoByBasehead, + github.CommitsComparison{ + Files: []*github.CommitFile{ + {Filename: github.Ptr("main.go")}, + }, + }, + ), + gh_mock.WithRequestMatch( + gh_mock.GetReposPullsFilesByOwnerByRepoByPullNumber, + []*github.CommitFile{ + {Filename: github.Ptr("README.md")}, + {Filename: github.Ptr("main.go")}, + }, + ), + ) + + // Create a GitHub client with the mocked HTTP client + gh := github.NewClient(mockedHTTPClient) + + // Use the custom type as the key + ctx := context.WithValue(context.Background(), githubClientKey, gh) + + // Create a mock store using the proper mocking pattern + mockStore := store_mocks.NewMockStore(t) + mockStore.On("GetUser", mock.Anything).Return(&model.User{ + ID: 1, + Login: "6543", + AccessToken: "token", + }, nil) + mockStore.On("GetRepoNameFallback", mock.Anything, mock.Anything).Return(&model.Repo{ + ID: 1, + ForgeRemoteID: "1", + Owner: "6543", + Name: "hello-world", + UserID: 1, + }, nil) + + // Set up context with mock store + ctx = store.InjectToContext(ctx, mockStore) + + // Create a mock client + c := &client{ + API: defaultAPI, + url: defaultURL, + } + + t.Run("convert push from webhook", func(t *testing.T) { + // Create a mock HTTP request with a push event payload + req := httptest.NewRequest("POST", "/hook", strings.NewReader(fixtures.HookPush)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GitHub-Event", "push") + + // Call the Hook function + repo, pipeline, err := c.Hook(ctx, req) + + assert.NoError(t, err) + assert.NotNil(t, repo) + assert.NotNil(t, pipeline) + assert.Equal(t, model.EventPush, pipeline.Event) + assert.Equal(t, "main", pipeline.Branch) + assert.Equal(t, "refs/heads/main", pipeline.Ref) + assert.Equal(t, "366701fde727cb7a9e7f21eb88264f59f6f9b89c", pipeline.Commit) + assert.Equal(t, "Fix multiline secrets replacer (#700)\n\n* Fix multiline secrets replacer\r\n\r\n* Add tests", pipeline.Message) + assert.Equal(t, "https://github.com/woodpecker-ci/woodpecker/commit/366701fde727cb7a9e7f21eb88264f59f6f9b89c", pipeline.ForgeURL) + assert.Equal(t, "6543", pipeline.Author) + assert.Equal(t, "https://avatars.githubusercontent.com/u/24977596?v=4", pipeline.Avatar) + assert.Equal(t, "admin@philipp.info", pipeline.Email) + assert.Equal(t, []string{"main.go"}, pipeline.ChangedFiles) + }) + + t.Run("convert pull request from webhook", func(t *testing.T) { + // Create a mock HTTP request with a pull request event payload + req := httptest.NewRequest("POST", "/hook", strings.NewReader(fixtures.HookPullRequest)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GitHub-Event", "pull_request") + + // Call the Hook function + repo, pipeline, err := c.Hook(ctx, req) + + assert.NoError(t, err) + assert.NotNil(t, repo) + assert.NotNil(t, pipeline) + assert.Equal(t, model.EventPull, pipeline.Event) + assert.Equal(t, "main", pipeline.Branch) + assert.Equal(t, "refs/pull/1/head", pipeline.Ref) + assert.Equal(t, "changes:main", pipeline.Refspec) + assert.Equal(t, "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", pipeline.Commit) + assert.Equal(t, "Update the README with new information", pipeline.Message) + assert.Equal(t, "Update the README with new information", pipeline.Title) + assert.Equal(t, "baxterthehacker", pipeline.Author) + assert.Equal(t, "https://avatars.githubusercontent.com/u/6752317?v=3", pipeline.Avatar) + assert.Equal(t, "octocat", pipeline.Sender) + assert.Equal(t, []string{"README.md", "main.go"}, pipeline.ChangedFiles) + }) + + t.Run("convert deployment from webhook", func(t *testing.T) { + // Create a mock HTTP request with a deployment event payload + req := httptest.NewRequest("POST", "/hook", strings.NewReader(fixtures.HookDeploy)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GitHub-Event", "deployment") + + // Call the Hook function + repo, pipeline, err := c.Hook(ctx, req) + + assert.NoError(t, err) + assert.NotNil(t, repo) + assert.NotNil(t, pipeline) + assert.Equal(t, model.EventDeploy, pipeline.Event) + assert.Equal(t, "main", pipeline.Branch) + assert.Equal(t, "refs/heads/main", pipeline.Ref) + assert.Equal(t, "9049f1265b7d61be4a8904a9a27120d2064dab3b", pipeline.Commit) + assert.Equal(t, "", pipeline.Message) + assert.Equal(t, "https://api.github.com/repos/baxterthehacker/public-repo/deployments/710692", pipeline.ForgeURL) + assert.Equal(t, "baxterthehacker", pipeline.Author) + assert.Equal(t, "https://avatars.githubusercontent.com/u/6752317?v=3", pipeline.Avatar) + }) + + t.Run("convert tag from webhook", func(t *testing.T) { + // Create a mock HTTP request with a tag event payload but push event header (tags create push events at github) + req := httptest.NewRequest("POST", "/hook", strings.NewReader(fixtures.HookTag)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GitHub-Event", "push") + + // Call the Hook function + repo, pipeline, err := c.Hook(ctx, req) + + assert.NoError(t, err) + assert.NotNil(t, repo) + assert.NotNil(t, pipeline) + assert.Equal(t, model.EventTag, pipeline.Event) + assert.Equal(t, "main", pipeline.Branch) + assert.Equal(t, "refs/tags/the-tag-v1", pipeline.Ref) + assert.Equal(t, "67012991d6c69b1c58378346fca366b864d8d1a1", pipeline.Commit) + assert.Equal(t, "Update .woodpecker.yml", pipeline.Message) + assert.Equal(t, "https://github.com/6543/test_ci_tmp/commit/67012991d6c69b1c58378346fca366b864d8d1a1", pipeline.ForgeURL) + assert.Equal(t, "6543", pipeline.Author) + assert.Equal(t, "https://avatars.githubusercontent.com/u/24977596?v=4", pipeline.Avatar) + assert.Equal(t, "6543@obermui.de", pipeline.Email) + assert.Empty(t, pipeline.ChangedFiles) + }) +} diff --git a/server/forge/github/parse.go b/server/forge/github/parse.go index 22f4e0ff6..0cba23ed0 100644 --- a/server/forge/github/parse.go +++ b/server/forge/github/parse.go @@ -22,12 +22,11 @@ import ( "net/http" "strings" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v75/github" "go.woodpecker-ci.org/woodpecker/v3/server/forge/common" "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/model" - "go.woodpecker-ci.org/woodpecker/v3/shared/utils" ) const ( @@ -56,7 +55,7 @@ const ( // parseHook parses a GitHub hook from an http.Request request and returns // Repo and Pipeline detail. If a hook type is unsupported nil values are returned. -func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, *model.Pipeline, error) { +func parseHook(r *http.Request, merge bool) (_ *github.PullRequest, _ *model.Repo, _ *model.Pipeline, currCommit, prevCommit string, _ error) { var reader io.Reader = r.Body if payload := r.FormValue(hookField); payload != "" { @@ -65,51 +64,52 @@ func parseHook(r *http.Request, merge bool) (*github.PullRequest, *model.Repo, * raw, err := io.ReadAll(reader) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, "", "", err } payload, err := github.ParseWebHook(github.WebHookType(r), raw) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, "", "", err } switch hook := payload.(type) { case *github.PushEvent: - repo, pipeline := parsePushHook(hook) - return nil, repo, pipeline, nil + repo, pipeline, curr, prev := parsePushHook(hook) + return nil, repo, pipeline, curr, prev, nil case *github.DeploymentEvent: repo, pipeline := parseDeployHook(hook) - return nil, repo, pipeline, nil + return nil, repo, pipeline, "", "", nil case *github.PullRequestEvent: - return parsePullHook(hook, merge) + pr, repo, pipeline, err := parsePullHook(hook, merge) + return pr, repo, pipeline, "", "", err case *github.ReleaseEvent: repo, pipeline := parseReleaseHook(hook) - return nil, repo, pipeline, nil + return nil, repo, pipeline, "", "", nil default: - return nil, nil, nil, &types.ErrIgnoreEvent{Event: github.Stringify(hook)} + return nil, nil, nil, "", "", &types.ErrIgnoreEvent{Event: github.Stringify(hook)} } } // parsePushHook parses a push hook and returns the Repo and Pipeline details. // If the commit type is unsupported nil values are returned. -func parsePushHook(hook *github.PushEvent) (*model.Repo, *model.Pipeline) { +func parsePushHook(hook *github.PushEvent) (_ *model.Repo, _ *model.Pipeline, curr, prev string) { if hook.Deleted != nil && *hook.Deleted { - return nil, nil + return nil, nil, "", "" } pipeline := &model.Pipeline{ - Event: model.EventPush, - Commit: hook.GetHeadCommit().GetID(), - Ref: hook.GetRef(), - ForgeURL: hook.GetHeadCommit().GetURL(), - Branch: strings.ReplaceAll(hook.GetRef(), "refs/heads/", ""), - Message: hook.GetHeadCommit().GetMessage(), - Email: hook.GetHeadCommit().GetAuthor().GetEmail(), - Avatar: hook.GetSender().GetAvatarURL(), - Author: hook.GetSender().GetLogin(), - Sender: hook.GetSender().GetLogin(), - ChangedFiles: getChangedFilesFromCommits(hook.Commits), + Event: model.EventPush, + Commit: hook.GetHeadCommit().GetID(), + Ref: hook.GetRef(), + ForgeURL: hook.GetHeadCommit().GetURL(), + Branch: strings.ReplaceAll(hook.GetRef(), "refs/heads/", ""), + Message: hook.GetHeadCommit().GetMessage(), + Email: hook.GetHeadCommit().GetAuthor().GetEmail(), + Avatar: hook.GetSender().GetAvatarURL(), + Author: hook.GetSender().GetLogin(), + Sender: hook.GetSender().GetLogin(), } + repo := convertRepoHook(hook.GetRepo()) if len(pipeline.Author) == 0 { pipeline.Author = hook.GetHeadCommit().GetAuthor().GetLogin() @@ -118,15 +118,15 @@ func parsePushHook(hook *github.PushEvent) (*model.Repo, *model.Pipeline) { // just kidding, this is actually a tag event. Why did this come as a push // event we'll never know! pipeline.Event = model.EventTag - pipeline.ChangedFiles = nil // For tags, if the base_ref (tag's base branch) is set, we're using it // as pipeline's branch so that we can filter events base on it if strings.HasPrefix(hook.GetBaseRef(), "refs/heads/") { pipeline.Branch = strings.ReplaceAll(hook.GetBaseRef(), "refs/heads/", "") } + return repo, pipeline, "", "" } - return convertRepoHook(hook.GetRepo()), pipeline + return repo, pipeline, hook.GetHeadCommit().GetID(), hook.GetBefore() } // parseDeployHook parses a deployment and returns the Repo and Pipeline details. @@ -254,14 +254,3 @@ func parseReleaseHook(hook *github.ReleaseEvent) (*model.Repo, *model.Pipeline) return convertRepo(hook.GetRepo()), pipeline } - -func getChangedFilesFromCommits(commits []*github.HeadCommit) []string { - // assume a capacity of 4 changed files per commit - files := make([]string, 0, len(commits)*4) - for _, cm := range commits { - files = append(files, cm.Added...) - files = append(files, cm.Removed...) - files = append(files, cm.Modified...) - } - return utils.DeduplicateStrings(files) -} diff --git a/server/forge/github/parse_test.go b/server/forge/github/parse_test.go index 7791dac34..4a61e0207 100644 --- a/server/forge/github/parse_test.go +++ b/server/forge/github/parse_test.go @@ -48,7 +48,9 @@ func testHookRequest(payload []byte, event string) *http.Request { func Test_parseHook(t *testing.T) { t.Run("ignore unsupported hook events", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequest), "issues") - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.Nil(t, r) assert.Nil(t, b) assert.Nil(t, p) @@ -57,7 +59,9 @@ func Test_parseHook(t *testing.T) { t.Run("skip skip push hook when action is deleted", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPushDeleted), hookPush) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.Nil(t, r) assert.Nil(t, b) assert.NoError(t, err) @@ -65,19 +69,22 @@ func Test_parseHook(t *testing.T) { }) t.Run("push hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPush), hookPush) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Equal(t, "2f780193b136b72bfea4eeb640786a8c4450c7a2", pc) + assert.Equal(t, "366701fde727cb7a9e7f21eb88264f59f6f9b89c", cc) assert.NoError(t, err) assert.Nil(t, p) assert.NotNil(t, r) assert.NotNil(t, b) assert.Equal(t, model.EventPush, b.Event) sort.Strings(b.ChangedFiles) - assert.Equal(t, []string{"pipeline/shared/replace_secrets.go", "pipeline/shared/replace_secrets_test.go"}, b.ChangedFiles) }) t.Run("PR hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequest), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -86,7 +93,9 @@ func Test_parseHook(t *testing.T) { }) t.Run("PR closed hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestClosed), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -96,7 +105,9 @@ func Test_parseHook(t *testing.T) { t.Run("reopen a pull", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestReopened), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -106,7 +117,9 @@ func Test_parseHook(t *testing.T) { t.Run("PR merged hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestMerged), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -116,7 +129,9 @@ func Test_parseHook(t *testing.T) { t.Run("PR edited hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestEdited), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -127,7 +142,9 @@ func Test_parseHook(t *testing.T) { t.Run("deploy hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookDeploy), hookDeploy) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -139,7 +156,9 @@ func Test_parseHook(t *testing.T) { t.Run("release hook", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookRelease), hookRelease) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -151,7 +170,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull review requested", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestReviewRequested), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.ErrorIs(t, err, &types.ErrIgnoreEvent{}) assert.Nil(t, r) assert.Nil(t, b) @@ -160,7 +181,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull milestoned", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestMilestoneAdded), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -207,7 +230,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request demilestoned", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestMilestoneRemoved), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -242,7 +267,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request labele added", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestLabelAdded), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -292,7 +319,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request got label removed", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestLabelRemoved), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -326,7 +355,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request got all label removed", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestLabelsCleared), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -355,7 +386,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request assigned", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestAssigneeAdded), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b) @@ -398,7 +431,9 @@ func Test_parseHook(t *testing.T) { t.Run("pull request unassigned", func(t *testing.T) { req := testHookRequest([]byte(fixtures.HookPullRequestAssigneeRemoved), hookPull) - p, r, b, err := parseHook(req, false) + p, r, b, cc, pc, err := parseHook(req, false) + assert.Empty(t, pc) + assert.Empty(t, cc) assert.NoError(t, err) assert.NotNil(t, r) assert.NotNil(t, b)