Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b7c55d3637
commit
d044a84456
|
|
@ -1 +1 @@
|
|||
4f276256d6ef947d1b8b12671dd6878f9fc513c3
|
||||
1841bc63148c1743d4334f8dbeffbd54dee24d83
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -156,7 +156,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.2.0', require: 'omniauth-ldap', feature_catego
|
|||
gem 'net-ldap', '~> 0.17.1', feature_category: :system_access
|
||||
|
||||
# API
|
||||
gem 'grape', '~> 2.0.0', feature_category: :api
|
||||
gem 'grape', '~> 2.1.0', feature_category: :api
|
||||
gem 'grape-entity', '~> 1.0.1', feature_category: :api
|
||||
gem 'grape-swagger', '~> 2.1.2', group: [:development, :test], feature_category: :api
|
||||
gem 'grape-swagger-entity', '~> 0.5.5', group: [:development, :test], feature_category: :api
|
||||
|
|
@ -756,4 +756,4 @@ gem 'paper_trail', '~> 15.0', feature_category: :shared
|
|||
|
||||
gem "i18n_data", "~> 0.13.1", feature_category: :system_access
|
||||
|
||||
gem "gitlab-cloud-connector", "~> 1.0.0", require: 'gitlab/cloud_connector', feature_category: :cloud_connector
|
||||
gem "gitlab-cloud-connector", "~> 1.4", require: 'gitlab/cloud_connector', feature_category: :cloud_connector
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@
|
|||
{"name":"gitaly","version":"17.8.4","platform":"ruby","checksum":"196d9735a83f8a7d396baa216b979eb0c801622d8b7573f90010338d5b0c7b4f"},
|
||||
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
|
||||
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.0.0","platform":"ruby","checksum":"edf2d13c698e1eb8a6828acefa7c12230f1e47d48212710f1617d5f986d051af"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.4.0","platform":"ruby","checksum":"01083b3a03f6db3d2da116cc7e8f3a121eaa91240a738a503b80199e6d8ccd90"},
|
||||
{"name":"gitlab-dangerfiles","version":"4.8.1","platform":"ruby","checksum":"bbad321c9638152a643d27a20b35ba1e2d8eddcc6bdfc4493d7b96e816ecf300"},
|
||||
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
|
||||
{"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"},
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
{"name":"googleapis-common-protos-types","version":"1.18.0","platform":"ruby","checksum":"280d19dcf431f86b9ff7ed8b23994915c5f4f7a0282ad3e870a2d3595976559f"},
|
||||
{"name":"googleauth","version":"1.8.1","platform":"ruby","checksum":"814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7"},
|
||||
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
||||
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
||||
{"name":"grape","version":"2.1.3","platform":"ruby","checksum":"62973acde2a89ac275baa9efb67adcc9240b83b0016ca561807f81a829a47e37"},
|
||||
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
||||
{"name":"grape-path-helpers","version":"2.0.1","platform":"ruby","checksum":"ad5216e52c6e796738a9118087352ab4c962900dbad1d8f8c0f96e093c6702d7"},
|
||||
{"name":"grape-swagger","version":"2.1.2","platform":"ruby","checksum":"8ad7bd53c8baee704575808875dba8c08d269c457db3cf8f1b8a2a1dbf827294"},
|
||||
|
|
@ -345,8 +345,8 @@
|
|||
{"name":"jira-ruby","version":"2.3.0","platform":"ruby","checksum":"abf26e6bff4a8ea40bae06f7df6276a5776905c63fb2070934823ca54f62eb62"},
|
||||
{"name":"jmespath","version":"1.6.2","platform":"ruby","checksum":"238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1"},
|
||||
{"name":"js_regex","version":"3.8.0","platform":"ruby","checksum":"7934bcdd5a0e6d5af4a520288fd4684a02a472ae55831d9178ccaf82356344b5"},
|
||||
{"name":"json","version":"2.10.1","platform":"java","checksum":"de07233fb74113af2186eb9342f8207c9be0faf289a1e2623c9b0acb8b0b0ee1"},
|
||||
{"name":"json","version":"2.10.1","platform":"ruby","checksum":"ddc88ad91a1baf3f0038c174f253af3b086d30dc74db17ca4259bbde982f94dc"},
|
||||
{"name":"json","version":"2.10.2","platform":"java","checksum":"fe31faac61ea21ea1448c35450183f84e85c2b94cc6522c241959ba9d1362006"},
|
||||
{"name":"json","version":"2.10.2","platform":"ruby","checksum":"34e0eada93022b2a0a3345bb0b5efddb6e9ff5be7c48e409cfb54ff8a36a8b06"},
|
||||
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
|
||||
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
|
||||
|
|
@ -407,7 +407,7 @@
|
|||
{"name":"multipart-post","version":"2.2.3","platform":"ruby","checksum":"462979de2971b8df33c2ee797fd497731617241f9dcd93960cc3caccb2dd13d8"},
|
||||
{"name":"murmurhash3","version":"0.1.7","platform":"ruby","checksum":"370a2ce2e9ab0711e51554e530b5f63956927a6554a296855f42a1a4a5ed0936"},
|
||||
{"name":"mustermann","version":"3.0.0","platform":"ruby","checksum":"6d3569aa3c3b2f048c60626f48d9b2d561cc8d2ef269296943b03da181c08b67"},
|
||||
{"name":"mustermann-grape","version":"1.0.2","platform":"ruby","checksum":"6f5309d6a338f801f211c644e8c2d3cc2577a8693f9cd51dadfdb29c1260f5fe"},
|
||||
{"name":"mustermann-grape","version":"1.1.0","platform":"ruby","checksum":"8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5"},
|
||||
{"name":"mutex_m","version":"0.3.0","platform":"ruby","checksum":"cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751"},
|
||||
{"name":"nap","version":"1.1.0","platform":"ruby","checksum":"949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576"},
|
||||
{"name":"nenv","version":"0.3.0","platform":"ruby","checksum":"d9de6d8fb7072228463bf61843159419c969edb34b3cef51832b516ae7972765"},
|
||||
|
|
@ -541,7 +541,6 @@
|
|||
{"name":"racc","version":"1.8.1","platform":"java","checksum":"54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98"},
|
||||
{"name":"racc","version":"1.8.1","platform":"ruby","checksum":"4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f"},
|
||||
{"name":"rack","version":"2.2.11","platform":"ruby","checksum":"424c49affa19081e9255d65d861f2d7bc7d8388edc0cb608b5e6caf1dd49bb8a"},
|
||||
{"name":"rack-accept","version":"0.4.5","platform":"ruby","checksum":"66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2"},
|
||||
{"name":"rack-attack","version":"6.7.0","platform":"ruby","checksum":"3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c"},
|
||||
{"name":"rack-cors","version":"2.0.2","platform":"ruby","checksum":"415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b"},
|
||||
{"name":"rack-oauth2","version":"2.2.1","platform":"ruby","checksum":"c73aa87c508043e2258f02b4fb110cacba9b37d2ccf884e22487d014a120d1a5"},
|
||||
|
|
|
|||
23
Gemfile.lock
23
Gemfile.lock
|
|
@ -742,7 +742,7 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
gitlab-chronic (0.10.6)
|
||||
numerizer (~> 0.2)
|
||||
gitlab-cloud-connector (1.0.0)
|
||||
gitlab-cloud-connector (1.4.0)
|
||||
activesupport (~> 7.0)
|
||||
jwt (~> 2.9.3)
|
||||
gitlab-dangerfiles (4.8.1)
|
||||
|
|
@ -922,13 +922,12 @@ GEM
|
|||
signet (>= 0.16, < 2.a)
|
||||
gpgme (2.0.24)
|
||||
mini_portile2 (~> 2.7)
|
||||
grape (2.0.0)
|
||||
activesupport (>= 5)
|
||||
builder
|
||||
grape (2.1.3)
|
||||
activesupport (>= 6)
|
||||
dry-types (>= 1.1)
|
||||
mustermann-grape (~> 1.0.0)
|
||||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
mustermann-grape (~> 1.1.0)
|
||||
rack (>= 2)
|
||||
zeitwerk
|
||||
grape-entity (1.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
multi_json (>= 1.3.2)
|
||||
|
|
@ -1052,7 +1051,7 @@ GEM
|
|||
character_set (~> 1.4)
|
||||
regexp_parser (~> 2.5)
|
||||
regexp_property_values (~> 1.0)
|
||||
json (2.10.1)
|
||||
json (2.10.2)
|
||||
json-jwt (1.16.6)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
|
|
@ -1187,7 +1186,7 @@ GEM
|
|||
murmurhash3 (0.1.7)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mustermann-grape (1.0.2)
|
||||
mustermann-grape (1.1.0)
|
||||
mustermann (>= 1.0.0)
|
||||
mutex_m (0.3.0)
|
||||
nap (1.1.0)
|
||||
|
|
@ -1492,8 +1491,6 @@ GEM
|
|||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.11)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
|
@ -2099,7 +2096,7 @@ DEPENDENCIES
|
|||
gitlab-active-context!
|
||||
gitlab-backup-cli!
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-cloud-connector (~> 1.0.0)
|
||||
gitlab-cloud-connector (~> 1.4)
|
||||
gitlab-dangerfiles (~> 4.8.0)
|
||||
gitlab-duo-workflow-service-client (~> 0.1)!
|
||||
gitlab-experiment (~> 0.9.1)
|
||||
|
|
@ -2145,7 +2142,7 @@ DEPENDENCIES
|
|||
google-protobuf (~> 3.25, >= 3.25.3)
|
||||
googleauth (~> 1.8.1)
|
||||
gpgme (~> 2.0.24)
|
||||
grape (~> 2.0.0)
|
||||
grape (~> 2.1.0)
|
||||
grape-entity (~> 1.0.1)
|
||||
grape-path-helpers (~> 2.0.1)
|
||||
grape-swagger (~> 2.1.2)
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@
|
|||
{"name":"gitaly","version":"17.8.4","platform":"ruby","checksum":"196d9735a83f8a7d396baa216b979eb0c801622d8b7573f90010338d5b0c7b4f"},
|
||||
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
|
||||
{"name":"gitlab-chronic","version":"0.10.6","platform":"ruby","checksum":"a244d11a1396d2aac6ae9b2f326adf1605ec1ad20c29f06e8b672047d415a9ac"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.0.0","platform":"ruby","checksum":"edf2d13c698e1eb8a6828acefa7c12230f1e47d48212710f1617d5f986d051af"},
|
||||
{"name":"gitlab-cloud-connector","version":"1.4.0","platform":"ruby","checksum":"01083b3a03f6db3d2da116cc7e8f3a121eaa91240a738a503b80199e6d8ccd90"},
|
||||
{"name":"gitlab-dangerfiles","version":"4.8.1","platform":"ruby","checksum":"bbad321c9638152a643d27a20b35ba1e2d8eddcc6bdfc4493d7b96e816ecf300"},
|
||||
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
|
||||
{"name":"gitlab-fog-azure-rm","version":"2.2.0","platform":"ruby","checksum":"31aa7c2170f57874053144e7f716ec9e15f32e71ffbd2c56753dce46e2e78ba9"},
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
{"name":"googleapis-common-protos-types","version":"1.18.0","platform":"ruby","checksum":"280d19dcf431f86b9ff7ed8b23994915c5f4f7a0282ad3e870a2d3595976559f"},
|
||||
{"name":"googleauth","version":"1.8.1","platform":"ruby","checksum":"814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7"},
|
||||
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
||||
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
||||
{"name":"grape","version":"2.1.3","platform":"ruby","checksum":"62973acde2a89ac275baa9efb67adcc9240b83b0016ca561807f81a829a47e37"},
|
||||
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
||||
{"name":"grape-path-helpers","version":"2.0.1","platform":"ruby","checksum":"ad5216e52c6e796738a9118087352ab4c962900dbad1d8f8c0f96e093c6702d7"},
|
||||
{"name":"grape-swagger","version":"2.1.2","platform":"ruby","checksum":"8ad7bd53c8baee704575808875dba8c08d269c457db3cf8f1b8a2a1dbf827294"},
|
||||
|
|
@ -348,8 +348,8 @@
|
|||
{"name":"jira-ruby","version":"2.3.0","platform":"ruby","checksum":"abf26e6bff4a8ea40bae06f7df6276a5776905c63fb2070934823ca54f62eb62"},
|
||||
{"name":"jmespath","version":"1.6.2","platform":"ruby","checksum":"238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1"},
|
||||
{"name":"js_regex","version":"3.8.0","platform":"ruby","checksum":"7934bcdd5a0e6d5af4a520288fd4684a02a472ae55831d9178ccaf82356344b5"},
|
||||
{"name":"json","version":"2.10.1","platform":"java","checksum":"de07233fb74113af2186eb9342f8207c9be0faf289a1e2623c9b0acb8b0b0ee1"},
|
||||
{"name":"json","version":"2.10.1","platform":"ruby","checksum":"ddc88ad91a1baf3f0038c174f253af3b086d30dc74db17ca4259bbde982f94dc"},
|
||||
{"name":"json","version":"2.10.2","platform":"java","checksum":"fe31faac61ea21ea1448c35450183f84e85c2b94cc6522c241959ba9d1362006"},
|
||||
{"name":"json","version":"2.10.2","platform":"ruby","checksum":"34e0eada93022b2a0a3345bb0b5efddb6e9ff5be7c48e409cfb54ff8a36a8b06"},
|
||||
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
|
||||
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
|
||||
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
|
||||
|
|
@ -410,7 +410,7 @@
|
|||
{"name":"multipart-post","version":"2.2.3","platform":"ruby","checksum":"462979de2971b8df33c2ee797fd497731617241f9dcd93960cc3caccb2dd13d8"},
|
||||
{"name":"murmurhash3","version":"0.1.7","platform":"ruby","checksum":"370a2ce2e9ab0711e51554e530b5f63956927a6554a296855f42a1a4a5ed0936"},
|
||||
{"name":"mustermann","version":"3.0.0","platform":"ruby","checksum":"6d3569aa3c3b2f048c60626f48d9b2d561cc8d2ef269296943b03da181c08b67"},
|
||||
{"name":"mustermann-grape","version":"1.0.2","platform":"ruby","checksum":"6f5309d6a338f801f211c644e8c2d3cc2577a8693f9cd51dadfdb29c1260f5fe"},
|
||||
{"name":"mustermann-grape","version":"1.1.0","platform":"ruby","checksum":"8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5"},
|
||||
{"name":"mutex_m","version":"0.3.0","platform":"ruby","checksum":"cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751"},
|
||||
{"name":"nap","version":"1.1.0","platform":"ruby","checksum":"949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576"},
|
||||
{"name":"nenv","version":"0.3.0","platform":"ruby","checksum":"d9de6d8fb7072228463bf61843159419c969edb34b3cef51832b516ae7972765"},
|
||||
|
|
@ -548,7 +548,6 @@
|
|||
{"name":"racc","version":"1.8.1","platform":"java","checksum":"54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98"},
|
||||
{"name":"racc","version":"1.8.1","platform":"ruby","checksum":"4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f"},
|
||||
{"name":"rack","version":"2.2.11","platform":"ruby","checksum":"424c49affa19081e9255d65d861f2d7bc7d8388edc0cb608b5e6caf1dd49bb8a"},
|
||||
{"name":"rack-accept","version":"0.4.5","platform":"ruby","checksum":"66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2"},
|
||||
{"name":"rack-attack","version":"6.7.0","platform":"ruby","checksum":"3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c"},
|
||||
{"name":"rack-cors","version":"2.0.2","platform":"ruby","checksum":"415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b"},
|
||||
{"name":"rack-oauth2","version":"2.2.1","platform":"ruby","checksum":"c73aa87c508043e2258f02b4fb110cacba9b37d2ccf884e22487d014a120d1a5"},
|
||||
|
|
@ -723,7 +722,6 @@
|
|||
{"name":"state_machines","version":"0.5.0","platform":"ruby","checksum":"23e6249d374a920b528dccade403518b4abbd83841a3e2c9ef13e6f1a009b102"},
|
||||
{"name":"state_machines-activemodel","version":"0.8.0","platform":"ruby","checksum":"e932dab190d4be044fb5f9cab01a3ea0b092c5f113d4676c6c0a0d49bf738d2c"},
|
||||
{"name":"state_machines-activerecord","version":"0.8.0","platform":"ruby","checksum":"072fb701b8ab03de0608297f6c55dc34ed096e556fa8f77e556f3c461c71aab6"},
|
||||
{"name":"stringio","version":"3.1.6","platform":"java","checksum":"dbdb1ee4e6d75782bbc7e8cc7d84cd05e592df50494f363011cc7cd48153bbf7"},
|
||||
{"name":"stringio","version":"3.1.6","platform":"ruby","checksum":"292c495d1657adfcdf0a32eecf12a60e6691317a500c3112ad3b2e31068274f5"},
|
||||
{"name":"strings","version":"0.2.1","platform":"ruby","checksum":"933293b3c95cf85b81eb44b3cf673e3087661ba739bbadfeadf442083158d6fb"},
|
||||
{"name":"strings-ansi","version":"0.2.0","platform":"ruby","checksum":"90262d760ea4a94cc2ae8d58205277a343409c288cbe7c29416b1826bd511c88"},
|
||||
|
|
|
|||
|
|
@ -754,7 +754,7 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
gitlab-chronic (0.10.6)
|
||||
numerizer (~> 0.2)
|
||||
gitlab-cloud-connector (1.0.0)
|
||||
gitlab-cloud-connector (1.4.0)
|
||||
activesupport (~> 7.0)
|
||||
jwt (~> 2.9.3)
|
||||
gitlab-dangerfiles (4.8.1)
|
||||
|
|
@ -934,13 +934,12 @@ GEM
|
|||
signet (>= 0.16, < 2.a)
|
||||
gpgme (2.0.24)
|
||||
mini_portile2 (~> 2.7)
|
||||
grape (2.0.0)
|
||||
activesupport (>= 5)
|
||||
builder
|
||||
grape (2.1.3)
|
||||
activesupport (>= 6)
|
||||
dry-types (>= 1.1)
|
||||
mustermann-grape (~> 1.0.0)
|
||||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
mustermann-grape (~> 1.1.0)
|
||||
rack (>= 2)
|
||||
zeitwerk
|
||||
grape-entity (1.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
multi_json (>= 1.3.2)
|
||||
|
|
@ -1069,7 +1068,7 @@ GEM
|
|||
character_set (~> 1.4)
|
||||
regexp_parser (~> 2.5)
|
||||
regexp_property_values (~> 1.0)
|
||||
json (2.10.1)
|
||||
json (2.10.2)
|
||||
json-jwt (1.16.6)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
|
|
@ -1204,7 +1203,7 @@ GEM
|
|||
murmurhash3 (0.1.7)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mustermann-grape (1.0.2)
|
||||
mustermann-grape (1.1.0)
|
||||
mustermann (>= 1.0.0)
|
||||
mutex_m (0.3.0)
|
||||
nap (1.1.0)
|
||||
|
|
@ -1515,8 +1514,6 @@ GEM
|
|||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.11)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
|
@ -2133,7 +2130,7 @@ DEPENDENCIES
|
|||
gitlab-active-context!
|
||||
gitlab-backup-cli!
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-cloud-connector (~> 1.0.0)
|
||||
gitlab-cloud-connector (~> 1.4)
|
||||
gitlab-dangerfiles (~> 4.8.0)
|
||||
gitlab-duo-workflow-service-client (~> 0.1)!
|
||||
gitlab-experiment (~> 0.9.1)
|
||||
|
|
@ -2179,7 +2176,7 @@ DEPENDENCIES
|
|||
google-protobuf (~> 3.25, >= 3.25.3)
|
||||
googleauth (~> 1.8.1)
|
||||
gpgme (~> 2.0.24)
|
||||
grape (~> 2.0.0)
|
||||
grape (~> 2.1.0)
|
||||
grape-entity (~> 1.0.1)
|
||||
grape-path-helpers (~> 2.0.1)
|
||||
grape-swagger (~> 2.1.2)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
query searchNamespacesWhereUserCanImportProjects($search: String) {
|
||||
currentUser {
|
||||
id
|
||||
groups(permissionScope: IMPORT_PROJECTS, search: $search) {
|
||||
groups(permissionScope: IMPORT_PROJECTS, search: $search, sort: SIMILARITY) {
|
||||
nodes {
|
||||
id
|
||||
fullPath
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ export default class MergeRequestTabs {
|
|||
this.pageLayout = document.querySelector('.layout-page');
|
||||
this.expandSidebar = document.querySelectorAll('.js-expand-sidebar, .js-sidebar-toggle');
|
||||
this.paddingTop = 16;
|
||||
this.actionRegex = /\/(commits|diffs|pipelines|reports(\/(.*))?)(\.html)?\/?$/;
|
||||
this.actionRegex = /\/(commits|diffs|pipelines|reports(?:\/[^/]+)?)(\.html)?\/?$/;
|
||||
|
||||
this.scrollPositions = {};
|
||||
|
||||
|
|
@ -448,7 +448,7 @@ export default class MergeRequestTabs {
|
|||
if (
|
||||
this.currentAction !== 'show' &&
|
||||
this.currentAction !== 'new' &&
|
||||
!/reports\/(.*)$/.test(pathname)
|
||||
!newStatePathname.endsWith(`/${this.currentAction}`)
|
||||
) {
|
||||
newStatePathname += `/${this.currentAction}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ export default {
|
|||
:work-item-iid="workItemIid"
|
||||
:is-resolving="isResolving"
|
||||
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
|
||||
:is-group-work-item="isGroupWorkItem"
|
||||
@startEditing="$emit('startEditing')"
|
||||
@resolve="resolveDiscussion"
|
||||
@startReplying="showReplyForm"
|
||||
|
|
@ -288,6 +289,7 @@ export default {
|
|||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:is-resolving="isResolving"
|
||||
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
|
||||
:is-group-work-item="isGroupWorkItem"
|
||||
@startReplying="showReplyForm"
|
||||
@startEditing="$emit('startEditing')"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
|
|
@ -323,6 +325,7 @@ export default {
|
|||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:is-resolving="isResolving"
|
||||
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
|
||||
:is-group-work-item="isGroupWorkItem"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', reply)"
|
||||
@reportAbuse="$emit('reportAbuse', reply)"
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isGroupWorkItem: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -430,6 +435,7 @@ export default {
|
|||
:has-replies="hasReplies"
|
||||
:full-path="fullPath"
|
||||
:hide-fullscreen-markdown-button="hideFullscreenMarkdownButton"
|
||||
:is-group-work-item="isGroupWorkItem"
|
||||
class="gl-mt-3"
|
||||
@cancelEditing="cancelEditing"
|
||||
@toggleResolveDiscussion="$emit('resolve')"
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ module Glql
|
|||
return if error_type
|
||||
|
||||
Gitlab::Metrics::GlqlSlis.record_apdex(
|
||||
labels: labels,
|
||||
labels: labels.merge(error_type: nil),
|
||||
success: duration_s <= query_urgency.duration
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ module WebHooks
|
|||
include ::Gitlab::Loggable
|
||||
|
||||
ENABLED_HOOK_TYPES = %w[ProjectHook].freeze
|
||||
MAX_FAILURES = 100
|
||||
FAILURE_THRESHOLD = 3
|
||||
EXCEEDED_FAILURE_THRESHOLD = FAILURE_THRESHOLD + 1
|
||||
|
||||
TEMPORARILY_DISABLED_FAILURE_THRESHOLD = 3
|
||||
# A webhook will be failing and being temporarily disabled for the max backoff of 1 day (`MAX_BACKOFF`)
|
||||
# for at least 1 month before it becomes permanently disabled on its 40th failure.
|
||||
# Exactly how quickly this happens depends on how frequently it triggers.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/503733#note_2217234805
|
||||
PERMANENTLY_DISABLED_FAILURE_THRESHOLD = 39
|
||||
|
||||
INITIAL_BACKOFF = 1.minute.freeze
|
||||
MAX_BACKOFF = 1.day.freeze
|
||||
MAX_BACKOFF_COUNT = 11
|
||||
|
|
@ -32,40 +37,40 @@ module WebHooks
|
|||
included do
|
||||
delegate :auto_disabling_enabled?, to: :class, private: true
|
||||
|
||||
# A hook is disabled if:
|
||||
# A webhook is disabled if:
|
||||
#
|
||||
# - we have exceeded the grace FAILURE_THRESHOLD (recent_failures > ?)
|
||||
# - and either:
|
||||
# - disabled_until is nil (i.e. this was set by WebHook#fail!)
|
||||
# - or disabled_until is in the future (i.e. this was set by WebHook#backoff!)
|
||||
# - OR silent mode is enabled.
|
||||
# - it has exceeded the grace TEMPORARILY_DISABLED_FAILURE_THRESHOLD (recent_failures > ?)
|
||||
# - AND the time period it was disabled for has not yet expired (disabled_until >= ?)
|
||||
# - OR it has reached the PERMANENTLY_DISABLED_FAILURE_THRESHOLD (recent_failures > ?)
|
||||
scope :disabled, -> do
|
||||
return all if Gitlab::SilentMode.enabled?
|
||||
return none unless auto_disabling_enabled?
|
||||
|
||||
where(
|
||||
'recent_failures > ? AND (disabled_until IS NULL OR disabled_until >= ?)',
|
||||
FAILURE_THRESHOLD,
|
||||
Time.current
|
||||
'(recent_failures > ? AND (disabled_until IS NULL OR disabled_until >= ?)) OR recent_failures > ?',
|
||||
TEMPORARILY_DISABLED_FAILURE_THRESHOLD,
|
||||
Time.current,
|
||||
PERMANENTLY_DISABLED_FAILURE_THRESHOLD
|
||||
)
|
||||
end
|
||||
|
||||
# A hook is executable if:
|
||||
# A webhook is executable if:
|
||||
#
|
||||
# - we have not yet exceeeded the grace FAILURE_THRESHOLD (recent_failures <= ?)
|
||||
# - OR we have exceeded the grace FAILURE_THRESHOLD and neither of the following is true:
|
||||
# - disabled_until is nil (i.e. this was set by WebHook#fail!)
|
||||
# - disabled_until is in the future (i.e. this was set by WebHook#backoff!)
|
||||
# - AND silent mode is not enabled.
|
||||
# - it has not exceeeded the grace TEMPORARILY_DISABLED_FAILURE_THRESHOLD (recent_failures <= ?)
|
||||
# - OR it has exceeded the grace TEMPORARILY_DISABLED_FAILURE_THRESHOLD and:
|
||||
# - it was temporarily disabled but can now be triggered again (disabled_until < ?)
|
||||
# - AND has not reached the PERMANENTLY_DISABLED_FAILURE_THRESHOLD (recent_failures <= ?)
|
||||
scope :executable, -> do
|
||||
return none if Gitlab::SilentMode.enabled?
|
||||
return all unless auto_disabling_enabled?
|
||||
|
||||
where(
|
||||
'recent_failures <= ? OR (recent_failures > ? AND (disabled_until IS NOT NULL) AND (disabled_until < ?))',
|
||||
FAILURE_THRESHOLD,
|
||||
FAILURE_THRESHOLD,
|
||||
Time.current
|
||||
'(recent_failures <= ? OR (recent_failures > ? AND disabled_until IS NOT NULL AND disabled_until < ?)) ' \
|
||||
'AND recent_failures <= ?',
|
||||
TEMPORARILY_DISABLED_FAILURE_THRESHOLD,
|
||||
TEMPORARILY_DISABLED_FAILURE_THRESHOLD,
|
||||
Time.current,
|
||||
PERMANENTLY_DISABLED_FAILURE_THRESHOLD
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -79,13 +84,18 @@ module WebHooks
|
|||
def temporarily_disabled?
|
||||
return false unless auto_disabling_enabled?
|
||||
|
||||
disabled_until.present? && disabled_until >= Time.current && recent_failures > FAILURE_THRESHOLD
|
||||
disabled_until.present? && disabled_until >= Time.current &&
|
||||
recent_failures.between?(TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1, PERMANENTLY_DISABLED_FAILURE_THRESHOLD)
|
||||
end
|
||||
|
||||
def permanently_disabled?
|
||||
return false unless auto_disabling_enabled?
|
||||
|
||||
recent_failures > FAILURE_THRESHOLD && disabled_until.blank?
|
||||
recent_failures > PERMANENTLY_DISABLED_FAILURE_THRESHOLD ||
|
||||
# Keep the old definition of permanently disabled just until we have migrated all records to the new definition
|
||||
# with `MigrateOldDisabledWebHookToNewState`
|
||||
# TODO Remove the next line as part of https://gitlab.com/gitlab-org/gitlab/-/issues/525446
|
||||
(recent_failures > TEMPORARILY_DISABLED_FAILURE_THRESHOLD && disabled_until.blank?)
|
||||
end
|
||||
|
||||
def enable!
|
||||
|
|
@ -99,7 +109,7 @@ module WebHooks
|
|||
save(validate: false)
|
||||
end
|
||||
|
||||
# Don't actually back-off until a grace level of FAILURE_THRESHOLD failures have been seen
|
||||
# Don't actually back-off until a grace level of TEMPORARILY_DISABLED_FAILURE_THRESHOLD failures have been seen
|
||||
# tracked in the recent_failures counter
|
||||
def backoff!
|
||||
return unless auto_disabling_enabled?
|
||||
|
|
@ -107,7 +117,7 @@ module WebHooks
|
|||
|
||||
attrs = { recent_failures: next_failure_count }
|
||||
|
||||
if recent_failures >= FAILURE_THRESHOLD
|
||||
if recent_failures >= TEMPORARILY_DISABLED_FAILURE_THRESHOLD
|
||||
attrs[:backoff_count] = next_backoff_count
|
||||
attrs[:disabled_until] = next_backoff.from_now
|
||||
end
|
||||
|
|
@ -120,17 +130,6 @@ module WebHooks
|
|||
save(validate: false)
|
||||
end
|
||||
|
||||
def failed!
|
||||
return unless auto_disabling_enabled?
|
||||
return unless recent_failures < MAX_FAILURES
|
||||
|
||||
attrs = { disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count }
|
||||
|
||||
assign_attributes(**attrs)
|
||||
logger.info(hook_id: id, action: 'disable', **attrs)
|
||||
save(validate: false)
|
||||
end
|
||||
|
||||
def next_backoff
|
||||
# Optimization to prevent expensive exponentiation and possible overflows
|
||||
return MAX_BACKOFF if backoff_count >= MAX_BACKOFF_COUNT
|
||||
|
|
@ -159,11 +158,11 @@ module WebHooks
|
|||
end
|
||||
|
||||
def next_failure_count
|
||||
recent_failures.succ.clamp(1, MAX_FAILURES)
|
||||
recent_failures.succ.clamp(1, PERMANENTLY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
def next_backoff_count
|
||||
backoff_count.succ.clamp(1, MAX_FAILURES)
|
||||
backoff_count.succ.clamp(1, PERMANENTLY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -202,10 +202,8 @@ class WebHookService
|
|||
def response_category(response)
|
||||
if response.success? || response.redirection?
|
||||
:ok
|
||||
elsif response.internal_server_error?
|
||||
:error
|
||||
else
|
||||
:failed
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ module WebHooks
|
|||
case response_category
|
||||
when :ok
|
||||
hook.enable!
|
||||
when :error
|
||||
# TODO remove handling of `:failed` as part of
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/525446
|
||||
when :error, :failed
|
||||
hook.backoff!
|
||||
when :failed
|
||||
hook.failed!
|
||||
end
|
||||
|
||||
hook.parent.update_last_webhook_failure(hook) if hook.parent
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
= hook.url
|
||||
|
||||
- if hook.rate_limited?
|
||||
= gl_badge_tag(_('Disabled'), variant: :danger)
|
||||
= gl_badge_tag(_('Webhooks|Rate limited'), variant: :danger)
|
||||
- elsif hook.permanently_disabled?
|
||||
= gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger)
|
||||
= gl_badge_tag(s_('Webhooks|Disabled'), variant: :danger)
|
||||
- elsif hook.temporarily_disabled?
|
||||
= gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning)
|
||||
= gl_badge_tag(s_('Webhooks|Temporarily disabled'), variant: :warning)
|
||||
|
||||
%div
|
||||
- hook.class.triggers.each_value do |trigger|
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- strong = { strong_start: '<strong>'.html_safe,
|
||||
strong_end: '</strong>'.html_safe }
|
||||
- help_link = link_to('', help_page_path('user/project/integrations/webhooks.md', anchor: 'auto-disabled-webhooks'), target: '_blank', rel: 'noopener noreferrer')
|
||||
- if hook.rate_limited?
|
||||
- placeholders = { limit: number_with_delimiter(hook.rate_limit),
|
||||
root_namespace: hook.parent.root_namespace.path }
|
||||
|
|
@ -8,14 +9,15 @@
|
|||
- c.with_body do
|
||||
= s_("Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. These webhooks are re-enabled automatically in the next minute.").html_safe % placeholders
|
||||
- elsif hook.permanently_disabled?
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
|
||||
- failure_count = { failure_count: hook.recent_failures }
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook disabled'),
|
||||
variant: :danger) do |c|
|
||||
- c.with_body do
|
||||
= safe_format(s_('Webhooks|The webhook failed to connect and is now disabled. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings.'), strong)
|
||||
= safe_format(s_('Webhooks|The webhook has %{help_link_start}failed%{help_link_end} %{failure_count} times consecutively and has been disabled. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings.'), strong, failure_count, tag_pair(help_link, :help_link_start, :help_link_end))
|
||||
- elsif hook.temporarily_disabled?
|
||||
- help_link = link_to('', help_page_path('user/project/integrations/webhooks.md', anchor: 'auto-disabled-webhooks'), target: '_blank', rel: 'noopener noreferrer')
|
||||
- retry_time = { retry_time: time_interval_in_words(hook.disabled_until - Time.now) }
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook fails to connect'),
|
||||
- failure_count = { failure_count: hook.recent_failures }
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook temporarily disabled'),
|
||||
variant: :warning) do |c|
|
||||
- c.with_body do
|
||||
= safe_format(s_('Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end} and is scheduled to retry in %{retry_time}. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings.'), retry_time, strong, tag_pair(help_link, :help_link_start, :help_link_end))
|
||||
= safe_format(s_('Webhooks|The webhook has %{help_link_start}failed%{help_link_end} %{failure_count} times consecutively and is disabled for %{retry_time}. To re-enable the webhook earlier, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings.'), retry_time, strong, failure_count, tag_pair(help_link, :help_link_start, :help_link_end))
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ if Gitlab.ee?
|
|||
Search::Zoekt::Task,
|
||||
Ai::CodeSuggestionEvent,
|
||||
Ai::DuoChatEvent,
|
||||
Ai::TroubleshootJobEvent,
|
||||
Vulnerabilities::Archive,
|
||||
Vulnerabilities::ArchivedRecord,
|
||||
Vulnerabilities::ArchiveExport
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
table_name: ai_troubleshoot_job_events
|
||||
classes:
|
||||
- Ai::TroubleshootJobEvent
|
||||
feature_categories:
|
||||
- duo_chat
|
||||
description: Database storage for raw /troubleshoot usage events. Partitioned by month.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184357
|
||||
milestone: '17.11'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateAiTroubleshootJobEvents < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.11'
|
||||
|
||||
def up
|
||||
# rubocop:disable Migration/Datetime -- "timestamp" is a column name
|
||||
create_table :ai_troubleshoot_job_events,
|
||||
options: 'PARTITION BY RANGE (timestamp)',
|
||||
primary_key: [:id, :timestamp] do |t|
|
||||
t.bigserial :id, null: false
|
||||
t.datetime_with_timezone :timestamp, null: false
|
||||
t.belongs_to :user, null: false
|
||||
t.references :job, null: false
|
||||
t.references :project, foreign_key: true, null: false
|
||||
t.timestamps_with_timezone null: false
|
||||
t.integer :event, null: false, limit: 2
|
||||
t.text :namespace_path, limit: 255
|
||||
t.jsonb :payload
|
||||
end
|
||||
# rubocop:enable Migration/Datetime
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :ai_troubleshoot_job_events
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTemporaryIndexToWebHooksForMigrateOldDisabledWebHookToNewState < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.11'
|
||||
|
||||
INDEX_NAME = 'tmp_index_web_hooks_on_disabled_until_recent_failures'
|
||||
TABLE = :web_hooks
|
||||
COLUMNS = [:id, :recent_failures, :disabled_until]
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index TABLE, COLUMNS, where: 'disabled_until is NULL', name: INDEX_NAME
|
||||
|
||||
connection.execute("ANALYZE #{TABLE}")
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name TABLE, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateOldDisabledWebHookToNewState < Gitlab::Database::Migration[2.2]
|
||||
BATCH_SIZE = 1000
|
||||
TABLE = 'web_hooks'
|
||||
SCOPE = ->(table) {
|
||||
table.where('recent_failures > 3').where(disabled_until: nil)
|
||||
}.freeze
|
||||
|
||||
NEW_RECENT_FAILURES = 40 # WebHooks::AutoDisabling::PERMANENTLY_DISABLED_FAILURE_THRESHOLD + 1
|
||||
NEW_BACKOFF_COUNT = 37 # NEW_RECENT_FAILURES - WebHooks::AutoDisabling::FAILURE_THRESHOLD
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
milestone '17.11'
|
||||
|
||||
def up
|
||||
disabled_until = Time.zone.now.to_fs(:db) # Specific time does not matter, just needs to be present
|
||||
|
||||
each_batch(TABLE, connection: connection, scope: SCOPE, of: BATCH_SIZE) do |batch, _batchable_model|
|
||||
batch.update_all(
|
||||
recent_failures: NEW_RECENT_FAILURES,
|
||||
backoff_count: NEW_BACKOFF_COUNT,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveTemporaryIndexFromWebHooksForMigrateDisabledWebHook < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.11'
|
||||
|
||||
INDEX_NAME = 'tmp_index_web_hooks_on_disabled_until_recent_failures'
|
||||
TABLE = :web_hooks
|
||||
COLUMNS = [:id, :recent_failures, :disabled_until]
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name TABLE, INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index TABLE, COLUMNS, where: 'disabled_until is NULL', name: INDEX_NAME
|
||||
|
||||
connection.execute("ANALYZE #{TABLE}")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
8ff13419285a7855b6a75e69ba63f10eb3ecb22655df946bb344d05201f7b6f6
|
||||
|
|
@ -0,0 +1 @@
|
|||
43a806f0236fcf8d57242c339a90e1afbb7c1ca5950d29423da5648eb4b855ff
|
||||
|
|
@ -0,0 +1 @@
|
|||
b90e017fcfdb70ab0d478f0d5fa2803fbcd6ee444c157902b34cab495496684b
|
||||
|
|
@ -0,0 +1 @@
|
|||
93b32fdd10b4eddaad779c8aa8ae8f0a9f0806549ee9539659fefa09e79a87ad
|
||||
|
|
@ -4459,6 +4459,21 @@ CREATE TABLE ai_duo_chat_events (
|
|||
)
|
||||
PARTITION BY RANGE ("timestamp");
|
||||
|
||||
CREATE TABLE ai_troubleshoot_job_events (
|
||||
id bigint NOT NULL,
|
||||
"timestamp" timestamp with time zone NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
job_id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
event smallint NOT NULL,
|
||||
namespace_path text,
|
||||
payload jsonb,
|
||||
CONSTRAINT check_29d6dbc329 CHECK ((char_length(namespace_path) <= 255))
|
||||
)
|
||||
PARTITION BY RANGE ("timestamp");
|
||||
|
||||
CREATE TABLE audit_events (
|
||||
id bigint NOT NULL,
|
||||
author_id bigint NOT NULL,
|
||||
|
|
@ -7975,6 +7990,15 @@ CREATE TABLE ai_testing_terms_acceptances (
|
|||
CONSTRAINT check_5efe98894e CHECK ((char_length(user_email) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ai_troubleshoot_job_events_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE ai_troubleshoot_job_events_id_seq OWNED BY ai_troubleshoot_job_events.id;
|
||||
|
||||
CREATE TABLE ai_user_metrics (
|
||||
user_id bigint NOT NULL,
|
||||
last_duo_activity_on date NOT NULL
|
||||
|
|
@ -26462,6 +26486,8 @@ ALTER TABLE ONLY ai_self_hosted_models ALTER COLUMN id SET DEFAULT nextval('ai_s
|
|||
|
||||
ALTER TABLE ONLY ai_settings ALTER COLUMN id SET DEFAULT nextval('ai_settings_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY ai_troubleshoot_job_events ALTER COLUMN id SET DEFAULT nextval('ai_troubleshoot_job_events_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY ai_vectorizable_files ALTER COLUMN id SET DEFAULT nextval('ai_vectorizable_files_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('alert_management_alert_assignees_id_seq'::regclass);
|
||||
|
|
@ -28451,6 +28477,9 @@ ALTER TABLE ONLY ai_settings
|
|||
ALTER TABLE ONLY ai_testing_terms_acceptances
|
||||
ADD CONSTRAINT ai_testing_terms_acceptances_pkey PRIMARY KEY (user_id);
|
||||
|
||||
ALTER TABLE ONLY ai_troubleshoot_job_events
|
||||
ADD CONSTRAINT ai_troubleshoot_job_events_pkey PRIMARY KEY (id, "timestamp");
|
||||
|
||||
ALTER TABLE ONLY ai_user_metrics
|
||||
ADD CONSTRAINT ai_user_metrics_pkey PRIMARY KEY (user_id);
|
||||
|
||||
|
|
@ -33329,6 +33358,12 @@ CREATE INDEX index_ai_settings_on_duo_workflow_service_account_user_id ON ai_set
|
|||
|
||||
CREATE UNIQUE INDEX index_ai_settings_on_singleton ON ai_settings USING btree (singleton);
|
||||
|
||||
CREATE INDEX index_ai_troubleshoot_job_events_on_job_id ON ONLY ai_troubleshoot_job_events USING btree (job_id);
|
||||
|
||||
CREATE INDEX index_ai_troubleshoot_job_events_on_project_id ON ONLY ai_troubleshoot_job_events USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_ai_troubleshoot_job_events_on_user_id ON ONLY ai_troubleshoot_job_events USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_ai_vectorizable_files_on_project_id ON ai_vectorizable_files USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_alert_assignees_on_alert_id ON alert_management_alert_assignees USING btree (alert_id);
|
||||
|
|
@ -43713,6 +43748,9 @@ ALTER TABLE ONLY boards_epic_board_positions
|
|||
ALTER TABLE ONLY external_status_checks
|
||||
ADD CONSTRAINT fk_rails_1f5a8aa809 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ai_troubleshoot_job_events
|
||||
ADD CONSTRAINT fk_rails_1fb7e812da FOREIGN KEY (project_id) REFERENCES projects(id);
|
||||
|
||||
ALTER TABLE ONLY dora_daily_metrics
|
||||
ADD CONSTRAINT fk_rails_1fd07aff6f FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -493,6 +493,34 @@ You might get an error that states
|
|||
|
||||
This error occurs when an unknown error occurs in ReAct agent. Try your request again. If the problem persists, report the issue to the GitLab support team.
|
||||
|
||||
## Feature not accessible or feature button not visible
|
||||
|
||||
If a feature is not working or a feature button (for example, **`/troubleshoot`**) is not visible:
|
||||
|
||||
1. Check if the feature's `unit_primitive` is listed in the [self-hosted models unit primitives list in the `gitlab-cloud-connector` gem configuration](https://gitlab.com/gitlab-org/cloud-connector/gitlab-cloud-connector/-/blob/main/config/services/self_hosted_models.yml).
|
||||
|
||||
If the feature is missing from this file, that could be the reason it's not accessible.
|
||||
|
||||
1. Optional. If the feature is not listed, you can verify this is the cause of the issue by setting the following in your GitLab instance:
|
||||
|
||||
```shell
|
||||
CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1
|
||||
```
|
||||
|
||||
Then restart GitLab and check if the feature becomes accessible.
|
||||
|
||||
> **Important**: After troubleshooting, restart GitLab **without** this flag set.
|
||||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
**Do not use `CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1` in production.** Development environments should closely mirror production, with no hidden flags or internal-only workarounds.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
1. To resolve this issue:
|
||||
- If you're a GitLab team member, contact the Custom Models team through the [`#g_custom_models` Slack channel](https://gitlab.enterprise.slack.com/archives/C06DCB3N96F).
|
||||
- If you're a customer, report the issue through [GitLab Support](https://about.gitlab.com/support/).
|
||||
|
||||
## Related topics
|
||||
|
||||
- [GitLab Duo troubleshooting](../../user/gitlab_duo_chat/troubleshooting.md)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ When working with GitLab Duo Chat, you might encounter the following issues.
|
|||
If the button is not visible in the upper-right of the UI,
|
||||
ensure GitLab Duo Chat [is enabled](turn_on_off.md).
|
||||
|
||||
The **GitLab Duo Chat** button is not displayed on personal projects,
|
||||
as well as
|
||||
The **GitLab Duo Chat** button is not displayed on
|
||||
[groups and projects with GitLab Duo features disabled](turn_on_off.md).
|
||||
|
||||
After you enable GitLab Duo Chat, it might take a few minutes for the
|
||||
|
|
|
|||
|
|
@ -36,11 +36,6 @@ Use it to filter and embed content from anywhere in the platform, using familiar
|
|||
Embed queries in Markdown code blocks.
|
||||
The rendered output of this query is called a view.
|
||||
|
||||
To test GLQL views:
|
||||
|
||||
- On GitLab Self-Managed, ask your administrator to enable the `glql_integration` feature flag on your instance.
|
||||
- On GitLab.com, contact your account representative.
|
||||
|
||||
Share your feedback in the [GLQL beta feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/509791).
|
||||
|
||||
## Query syntax
|
||||
|
|
|
|||
|
|
@ -30,14 +30,12 @@ This feature is available for testing, but not ready for production use.
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
In a GitLab Query Language (GLQL) [query](_index.md#query-syntax), a field is the leftmost part
|
||||
of the expression.
|
||||
In queries, fields follow the syntax of `<field> <operator> <value> and ...`,
|
||||
With GitLab Query Language (GLQL), fields are used to:
|
||||
|
||||
In a [GLQL view](_index.md#glql-views), fields are included as a comma-separated list of tokens in
|
||||
the `fields:` option.
|
||||
- Filter the results returned from a [GLQL query](_index.md#query-syntax).
|
||||
- Control the details displayed in a [GLQL view](_index.md#presentation-syntax).
|
||||
|
||||
This page lists fields available to use as filters when querying issues or work items.
|
||||
The following fields are available:
|
||||
|
||||
## Type
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
|
|
@ -481,6 +481,8 @@ To optimize your webhook receivers:
|
|||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/329849) for project webhooks in GitLab 15.7. Feature flag `web_hooks_disable_failed` removed.
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385902) for group webhooks in GitLab 15.10.
|
||||
- [Disabled on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/issues/390157) in GitLab 15.10 [with a flag](../../../administration/feature_flags.md) named `auto_disabling_web_hooks`.
|
||||
- **Fails to connect** and **Failing to connect** [renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166329) to **Disabled** and **Temporarily disabled** in GitLab 17.11.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166329) to become permanently disabled after 40 consecutive failures in GitLab 17.11.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -500,24 +502,34 @@ To view auto-disabled webhooks:
|
|||
|
||||
In the webhook list, auto-disabled webhooks display as:
|
||||
|
||||
- **Fails to connect** for [temporarily disabled](#temporarily-disabled-webhooks) webhooks
|
||||
- **Failed to connect** for [permanently disabled](#permanently-disabled-webhooks) webhooks
|
||||
- **Temporarily disabled** for [temporarily disabled](#temporarily-disabled-webhooks) webhooks
|
||||
- **Disabled** for [permanently disabled](#permanently-disabled-webhooks) webhooks
|
||||
|
||||

|
||||

|
||||
|
||||
#### Temporarily disabled webhooks
|
||||
|
||||
Webhooks are temporarily disabled if they:
|
||||
Webhooks are temporarily disabled if they fail four consecutive times.
|
||||
If webhooks fail 40 consecutive times, they become [permanently disabled](#permanently-disabled-webhooks).
|
||||
|
||||
- Return response codes in the `5xx` range.
|
||||
- Experience a [timeout](../../gitlab_com/_index.md#webhooks).
|
||||
- Encounter other HTTP errors.
|
||||
Failure occurs when:
|
||||
|
||||
These webhooks are initially disabled for one minute, with the duration extending on subsequent failures up to 24 hours.
|
||||
- The [webhook receiver](#webhook-receiver-requirements) returns a response code in the `4xx` or `5xx` range.
|
||||
- The webhook experiences a [timeout](../../gitlab_com/_index.md#webhooks) when attempting to connect to the webhook receiver.
|
||||
- The webhook encounters other HTTP errors.
|
||||
|
||||
Temporarily disabled webhooks are initially disabled for one minute,
|
||||
with the duration extending on subsequent failures up to 24 hours.
|
||||
After this period has elapsed, these webhooks are automatically re-enabled.
|
||||
|
||||
#### Permanently disabled webhooks
|
||||
|
||||
Webhooks are permanently disabled if they return response codes in the `4xx` range, indicating a misconfiguration.
|
||||
Webhooks are permanently disabled if they fail 40 consecutive times.
|
||||
Unlike [temporarily disabled webhooks](#temporarily-disabled-webhooks), these webhooks are not automatically re-enabled.
|
||||
|
||||
Webhooks that were permanently disabled in GitLab 17.10 and earlier underwent a data migration.
|
||||
These webhooks might display four failures in [**Recent events**](#view-webhook-request-history)
|
||||
even though the UI might state they have 40 failures.
|
||||
|
||||
#### Re-enable disabled webhooks
|
||||
|
||||
|
|
@ -528,10 +540,7 @@ Webhooks are permanently disabled if they return response codes in the `4xx` ran
|
|||
|
||||
{{< /history >}}
|
||||
|
||||
To re-enable a temporarily or permanently disabled webhook:
|
||||
|
||||
- [Send a test request](#test-a-webhook) to the webhook.
|
||||
|
||||
To re-enable a disabled webhook, [send a test request](#test-a-webhook).
|
||||
The webhook is re-enabled if the test request returns a response code in the `2xx` range.
|
||||
|
||||
### Delivery headers
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ module API
|
|||
documentation: { example: 'k813f89485474661234z7109cve5709eFFFFFFFF' }
|
||||
requires :same_file_name, same_as: :file_name
|
||||
end
|
||||
get '*file_name/*signature/*same_file_name', format: false, urgency: :low do
|
||||
get '*file_name/*signature/*same_file_name', format: true, urgency: :low do
|
||||
bad_request!('Missing checksum header') if headers['Symbolchecksum'].blank?
|
||||
|
||||
project_or_group_without_auth
|
||||
|
|
|
|||
|
|
@ -631,7 +631,9 @@ module API
|
|||
end
|
||||
|
||||
def render_api_error!(message, status)
|
||||
render_structured_api_error!({ 'message' => message }, status)
|
||||
error_message = message.is_a?(ActiveModel::Errors) ? message.to_hash : message
|
||||
|
||||
render_structured_api_error!({ 'message' => error_message }, status)
|
||||
end
|
||||
|
||||
def render_structured_api_error!(hash, status)
|
||||
|
|
@ -675,7 +677,7 @@ module API
|
|||
'500 Internal Server Error'
|
||||
end
|
||||
|
||||
rack_response({ 'message' => response_message }.to_json, 500)
|
||||
error!({ 'message' => response_message }, 500)
|
||||
end
|
||||
|
||||
# project helpers
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ module API
|
|||
module Slack
|
||||
module Request
|
||||
VERIFICATION_VERSION = 'v0'
|
||||
VERIFICATION_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp'
|
||||
VERIFICATION_SIGNATURE_HEADER = 'X-Slack-Signature'
|
||||
VERIFICATION_TIMESTAMP_HEADER = 'x-slack-request-timestamp'
|
||||
VERIFICATION_SIGNATURE_HEADER = 'x-slack-signature'
|
||||
VERIFICATION_DELIMITER = ':'
|
||||
VERIFICATION_HMAC_ALGORITHM = 'sha256'
|
||||
VERIFICATION_TIMESTAMP_EXPIRY = 1.minute.to_i
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ module API
|
|||
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
|
||||
route_setting :authorization, job_token_policies: :read_packages,
|
||||
allow_public_access_for_enabled_project_features: :package_registry
|
||||
get '*package_name/-/*file_name', format: false do
|
||||
get '*package_name/-/*file_name', format: true do
|
||||
authorize_read_package!(project)
|
||||
|
||||
package = ::Packages::Npm::Package
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ module API
|
|||
requires :package_version, type: String, allow_blank: false, desc: 'The NuGet package version',
|
||||
regexp: Gitlab::Regex.nuget_version_regex, documentation: { example: '1.0.1' }
|
||||
end
|
||||
delete '*package_name/*package_version', format: false, urgency: :low do
|
||||
delete '*package_name/*package_version', format: true, urgency: :low do
|
||||
authorize_destroy_package!(project_or_group)
|
||||
|
||||
destroy_conditionally!(find_package) do |package|
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ module API
|
|||
route_setting :authentication, job_token_allowed: true
|
||||
route_setting :authorization, job_token_policies: :read_releases,
|
||||
allow_public_access_for_enabled_project_features: [:repository, :releases]
|
||||
get ':id/releases/:tag_name/downloads/*direct_asset_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||
get ':id/releases/:tag_name/downloads/*direct_asset_path', format: true, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||
authorize_read_code!
|
||||
|
||||
not_found! unless release
|
||||
|
|
@ -196,7 +196,7 @@ module API
|
|||
route_setting :authentication, job_token_allowed: true
|
||||
route_setting :authorization, job_token_policies: :read_releases,
|
||||
allow_public_access_for_enabled_project_features: [:repository, :releases]
|
||||
get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||
get ':id/releases/permalink/latest(/)(*suffix_path)', format: true, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||
authorize_read_code!
|
||||
|
||||
# Try to find the latest release
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ module API
|
|||
]
|
||||
tags %w[terraform_registry]
|
||||
end
|
||||
get format: false do
|
||||
get format: true do
|
||||
presenter = ::Terraform::ModuleVersionPresenter.new(package, params[:module_system])
|
||||
present presenter, with: ::API::Entities::Terraform::ModuleVersion
|
||||
end
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ module API
|
|||
params do
|
||||
use :terraform_get
|
||||
end
|
||||
get format: false do
|
||||
get format: true do
|
||||
present_package_file
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ module Gitlab
|
|||
|
||||
def safe_parameters(request)
|
||||
loggable_params = super
|
||||
settings = request.env[Grape::Env::API_ENDPOINT]&.route&.settings
|
||||
options = request.env[Grape::Env::API_ENDPOINT]&.route&.options
|
||||
|
||||
return loggable_params unless settings&.key?(:log_safety)
|
||||
return loggable_params unless options&.dig(:settings, :log_safety)
|
||||
|
||||
settings[:log_safety][:safe].each do |key|
|
||||
options[:settings][:log_safety][:safe].each do |key|
|
||||
loggable_params[key] = request.params[key] if loggable_params.key?(key)
|
||||
end
|
||||
|
||||
settings[:log_safety][:unsafe].each do |key|
|
||||
options[:settings][:log_safety][:unsafe].each do |key|
|
||||
loggable_params[key] = @replacement if loggable_params.key?(key)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ module Tasks
|
|||
|
||||
row << [
|
||||
"`#{route_path(route)}`",
|
||||
route.description
|
||||
route.options[:description]
|
||||
]
|
||||
|
||||
markdown_row(row)
|
||||
|
|
@ -155,15 +155,15 @@ module Tasks
|
|||
end
|
||||
|
||||
def allowed_route?(route)
|
||||
route.settings.dig(:authentication, :job_token_allowed)
|
||||
route.options.dig(:settings, :authentication, :job_token_allowed)
|
||||
end
|
||||
|
||||
def skip_route?(route)
|
||||
route.settings.dig(:authorization, :skip_job_token_policies)
|
||||
route.options.dig(:settings, :authorization, :skip_job_token_policies)
|
||||
end
|
||||
|
||||
def policies_for(route)
|
||||
Array(route.settings.dig(:authorization, :job_token_policies))
|
||||
Array(route.options.dig(:settings, :authorization, :job_token_policies))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -66072,6 +66072,9 @@ msgstr ""
|
|||
msgid "Webhooks|Description (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Do not show sensitive data such as tokens in the UI."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66084,12 +66087,6 @@ msgstr ""
|
|||
msgid "Webhooks|Enable SSL verification"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Failed to connect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Fails to connect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Feature flag events"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66150,6 +66147,9 @@ msgstr ""
|
|||
msgid "Webhooks|Project or group access token events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Rate limited"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Regular expression"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66189,6 +66189,9 @@ msgstr ""
|
|||
msgid "Webhooks|Tag push events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Temporarily disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|The URL must be percent-encoded if it contains one or more special characters."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -66198,10 +66201,10 @@ msgstr ""
|
|||
msgid "Webhooks|The secret token is cleared on save unless it is updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|The webhook %{help_link_start}failed to connect%{help_link_end} and is scheduled to retry in %{retry_time}. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings."
|
||||
msgid "Webhooks|The webhook has %{help_link_start}failed%{help_link_end} %{failure_count} times consecutively and has been disabled. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|The webhook failed to connect and is now disabled. To re-enable the webhook, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings."
|
||||
msgid "Webhooks|The webhook has %{help_link_start}failed%{help_link_end} %{failure_count} times consecutively and is disabled for %{retry_time}. To re-enable the webhook earlier, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Trigger"
|
||||
|
|
@ -66219,15 +66222,12 @@ msgstr ""
|
|||
msgid "Webhooks|Webhook disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhook failed to connect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhook fails to connect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhook rate limit has been reached"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhook temporarily disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. These webhooks are re-enabled automatically in the next minute."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -45,4 +45,4 @@ tracer:
|
|||
enabled: false
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/471172
|
||||
gitlab_http_router:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ RSpec.describe Glql::BaseController, feature_category: :integrations do
|
|||
|
||||
it 'tracks SLI metrics for each successful glql query' do
|
||||
expect(Gitlab::Metrics::GlqlSlis).to receive(:record_apdex).with({
|
||||
labels: qlql_sli_labels,
|
||||
labels: qlql_sli_labels.merge(error_type: nil),
|
||||
success: true
|
||||
})
|
||||
|
||||
|
|
@ -52,6 +52,12 @@ RSpec.describe Glql::BaseController, feature_category: :integrations do
|
|||
|
||||
execute_request
|
||||
end
|
||||
|
||||
it 'does not fail when SLIs were initialized' do
|
||||
Gitlab::Metrics::GlqlSlis.initialize_slis!
|
||||
|
||||
expect { execute_request }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a single ActiveRecord::QueryAborted error occurs' do
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ RSpec.describe 'Database schema',
|
|||
abuse_report_notes: %w[discussion_id],
|
||||
ai_code_suggestion_events: %w[user_id],
|
||||
ai_duo_chat_events: %w[user_id organization_id],
|
||||
ai_troubleshoot_job_events: %w[user_id job_id],
|
||||
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_app_id eks_account_id
|
||||
eks_access_key_id],
|
||||
approvals: %w[user_id project_id],
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :permanently_disabled do
|
||||
recent_failures { WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1 }
|
||||
recent_failures { WebHooks::AutoDisabling::PERMANENTLY_DISABLED_FAILURE_THRESHOLD + 1 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -137,4 +137,19 @@ describe('ImportTargetDropdown', () => {
|
|||
{ text: 'match2', value: 'match2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts namespaces based on similarity to input', async () => {
|
||||
createComponent();
|
||||
|
||||
findListbox().vm.$emit('search', 'sortme');
|
||||
|
||||
await waitForQuery();
|
||||
|
||||
expect(findListboxGroupsItems()).toEqual([
|
||||
{ text: 'sortme', value: 'sortme' },
|
||||
{ text: 'sortmea', value: 'sortmea' },
|
||||
{ text: 'sortmeaa', value: 'sortmeaa' },
|
||||
{ text: 'sortmeab', value: 'sortmeab' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export const mockAvailableNamespaces = [
|
|||
mockGroupFactory('match1'),
|
||||
mockGroupFactory('unrelated'),
|
||||
mockGroupFactory('match2'),
|
||||
mockGroupFactory('sortme'),
|
||||
mockGroupFactory('sortmea'),
|
||||
mockGroupFactory('sortmeaa'),
|
||||
mockGroupFactory('sortmeab'),
|
||||
];
|
||||
|
||||
export const mockNamespacesResponse = {
|
||||
|
|
|
|||
|
|
@ -310,6 +310,30 @@ describe('MergeRequestTabs', () => {
|
|||
|
||||
expect(testContext.subject('show')).toBe('/foo/bar/-/merge_requests/1');
|
||||
});
|
||||
|
||||
it.each`
|
||||
pathname | action | expected
|
||||
${'/group/reports/project/-/merge_requests/1'} | ${'show'} | ${'/group/reports/project/-/merge_requests/1'}
|
||||
${'/group/reports/project/-/merge_requests/1'} | ${'reports'} | ${'/group/reports/project/-/merge_requests/1/reports'}
|
||||
${'/group/reports/project/-/merge_requests/1/reports'} | ${'reports'} | ${'/group/reports/project/-/merge_requests/1/reports'}
|
||||
${'/group/reports/project/-/merge_requests/1/reports'} | ${'show'} | ${'/group/reports/project/-/merge_requests/1'}
|
||||
${'/group/project/-/merge_requests/1/diffs'} | ${'commits'} | ${'/group/project/-/merge_requests/1/commits'}
|
||||
${'/group/project/-/merge_requests/1/commits'} | ${'diffs'} | ${'/group/project/-/merge_requests/1/diffs'}
|
||||
${'/group/project/-/merge_requests/1/reports/security'} | ${'show'} | ${'/group/project/-/merge_requests/1'}
|
||||
${'/group/project/-/merge_requests/1/reports/security'} | ${'reports'} | ${'/group/project/-/merge_requests/1/reports'}
|
||||
${'/group/project/-/merge_requests/1/commits'} | ${'commits'} | ${'/group/project/-/merge_requests/1/commits'}
|
||||
${'/group/project/-/merge_requests/1/diffs/'} | ${'show'} | ${'/group/project/-/merge_requests/1'}
|
||||
${'/group/project/-/merge_requests/1/commits.html'} | ${'show'} | ${'/group/project/-/merge_requests/1'}
|
||||
`(
|
||||
'updates URL to $expected if current URL is $pathname and new action is $action',
|
||||
({ pathname, action, expected }) => {
|
||||
setLocation({
|
||||
pathname,
|
||||
});
|
||||
|
||||
expect(testContext.subject(action)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('expandViewContainer', () => {
|
||||
|
|
@ -559,13 +583,14 @@ describe('MergeRequestTabs', () => {
|
|||
|
||||
describe('getActionFromHref', () => {
|
||||
it.each`
|
||||
pathName | action
|
||||
${'/user/pipelines/-/merge_requests/1/diffs'} | ${'diffs'}
|
||||
${'/user/diffs/-/merge_requests/1/pipelines'} | ${'pipelines'}
|
||||
${'/user/pipelines/-/merge_requests/1/commits'} | ${'commits'}
|
||||
${'/user/pipelines/1/-/merge_requests/1/diffs'} | ${'diffs'}
|
||||
${'/user/pipelines/-/merge_requests/1'} | ${'show'}
|
||||
${'/user/pipelines/-/merge_requests/1/reports'} | ${'reports'}
|
||||
pathName | action
|
||||
${'/user/pipelines/-/merge_requests/1/diffs'} | ${'diffs'}
|
||||
${'/user/diffs/-/merge_requests/1/pipelines'} | ${'pipelines'}
|
||||
${'/user/pipelines/-/merge_requests/1/commits'} | ${'commits'}
|
||||
${'/user/pipelines/1/-/merge_requests/1/diffs'} | ${'diffs'}
|
||||
${'/user/pipelines/-/merge_requests/1'} | ${'show'}
|
||||
${'/user/pipelines/-/merge_requests/1/reports'} | ${'reports'}
|
||||
${'/group/reports/project/-/merge_requests/1/reports'} | ${'reports'}
|
||||
`('returns $action for $location', ({ pathName, action }) => {
|
||||
expect(getActionFromHref(pathName)).toBe(action);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe API::API do
|
||||
describe '.prefix' do
|
||||
it 'has a prefix defined' do
|
||||
expect(described_class.prefix).to eq :api
|
||||
expect(described_class.prefix).to eq 'api'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::FilterParameters do
|
|||
subject { described_class.new }
|
||||
|
||||
describe ".parameters" do
|
||||
let(:route) { instance_double('Grape::Router::Route', settings: settings) }
|
||||
let(:route) { instance_double('Grape::Router::Route', options: options) }
|
||||
let(:endpoint) { instance_double('Grape::Endpoint', route: route) }
|
||||
|
||||
let(:env) do
|
||||
|
|
@ -24,7 +24,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::FilterParameters do
|
|||
end
|
||||
|
||||
context 'when the log_safety setting is provided' do
|
||||
let(:settings) { { log_safety: { safe: %w[foo bar key], unsafe: %w[oof rab value] } } }
|
||||
let(:options) { { settings: { log_safety: { safe: %w[foo bar key], unsafe: %w[oof rab value] } } } }
|
||||
|
||||
it 'includes safe parameters, and filters unsafe ones' do
|
||||
data = subject.parameters(mock_request, nil)
|
||||
|
|
@ -42,7 +42,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::FilterParameters do
|
|||
end
|
||||
|
||||
context 'when the log_safety is not provided' do
|
||||
let(:settings) { {} }
|
||||
let(:options) { {} }
|
||||
|
||||
it 'behaves like the normal parameter filter' do
|
||||
data = subject.parameters(mock_request, nil)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'fast_spec_helper'
|
||||
require 'batch-loader'
|
||||
|
||||
RSpec.describe Gitlab::Utils::BatchLoader do
|
||||
RSpec.describe Gitlab::Utils::BatchLoader, feature_category: :shared do
|
||||
let(:stubbed_loader) do
|
||||
double( # rubocop:disable RSpec/VerifiedDoubles
|
||||
'Loader',
|
||||
|
|
@ -54,6 +54,7 @@ RSpec.describe Gitlab::Utils::BatchLoader do
|
|||
|
||||
described_class.clear_key(:my_batch_name)
|
||||
|
||||
# .to_i triggers loading of lazy value
|
||||
test_module.lazy_method(4).to_i
|
||||
test_module.lazy_method_same_batch_key(5).to_i
|
||||
test_module.lazy_method_other_batch_key(6).to_i
|
||||
|
|
@ -64,6 +65,7 @@ RSpec.describe Gitlab::Utils::BatchLoader do
|
|||
end
|
||||
|
||||
it 'clears loaded values which match the specified batch key' do
|
||||
# .to_i triggers loading of lazy value
|
||||
test_module.lazy_method(1).to_i
|
||||
test_module.lazy_method_same_batch_key(2).to_i
|
||||
test_module.lazy_method_other_batch_key(3).to_i
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe MigrateOldDisabledWebHookToNewState, :freeze_time, feature_category: :webhooks, migration_version: 20250206114301 do
|
||||
let!(:web_hooks) { table(:web_hooks) }
|
||||
|
||||
let!(:non_disabled_webhook) { web_hooks.create!(recent_failures: 3, backoff_count: 1) }
|
||||
let!(:legacy_permanently_disabled_webhook) { web_hooks.create!(recent_failures: 4, backoff_count: 1) }
|
||||
let!(:temporarily_disabled_webhook) do
|
||||
web_hooks.create!(recent_failures: 4, backoff_count: 1, disabled_until: Time.current + 1.hour)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'migrates legacy permanently disabled web hooks to new permanently disabled state' do
|
||||
migrate!
|
||||
|
||||
[non_disabled_webhook, temporarily_disabled_webhook, legacy_permanently_disabled_webhook].each(&:reload)
|
||||
|
||||
expect(non_disabled_webhook.recent_failures).to eq(3)
|
||||
expect(non_disabled_webhook.backoff_count).to eq(1)
|
||||
expect(non_disabled_webhook.disabled_until).to be_nil
|
||||
|
||||
expect(temporarily_disabled_webhook.recent_failures).to eq(4)
|
||||
expect(temporarily_disabled_webhook.backoff_count).to eq(1)
|
||||
expect(temporarily_disabled_webhook.disabled_until).to eq(Time.current + 1.hour)
|
||||
|
||||
expect(legacy_permanently_disabled_webhook.recent_failures).to eq(40)
|
||||
expect(legacy_permanently_disabled_webhook.backoff_count).to eq(37)
|
||||
expect(legacy_permanently_disabled_webhook.disabled_until).to eq(Time.current)
|
||||
|
||||
expect(web_hooks.where(disabled_until: nil).where('recent_failures > 3').count).to eq(0)
|
||||
|
||||
expect(ProjectHook.executable.pluck_primary_key).to contain_exactly(non_disabled_webhook.id)
|
||||
expect(ProjectHook.disabled.pluck_primary_key).to contain_exactly(
|
||||
temporarily_disabled_webhook.id,
|
||||
legacy_permanently_disabled_webhook.id
|
||||
)
|
||||
end
|
||||
|
||||
it 'migrates in batches' do
|
||||
web_hooks.create!(recent_failures: 4, backoff_count: 1)
|
||||
web_hooks.create!(recent_failures: 4, backoff_count: 1)
|
||||
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
disabled_until = Time.zone.now.to_fs(:db)
|
||||
|
||||
expect do
|
||||
migrate!
|
||||
end.to make_queries_matching(
|
||||
/UPDATE "web_hooks" SET "recent_failures" = 40, "backoff_count" = 37, "disabled_until" = '#{disabled_until}'/,
|
||||
2
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6439,7 +6439,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
it 'executes hooks which were backed off and are no longer backed off' do
|
||||
project = create(:project)
|
||||
hook = create(:project_hook, project: project, push_events: true)
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.succ.times { hook.backoff! }
|
||||
WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD.succ.times { hook.backoff! }
|
||||
|
||||
expect_any_instance_of(ProjectHook).to receive(:async_execute).once
|
||||
|
||||
|
|
|
|||
|
|
@ -582,8 +582,10 @@ RSpec.describe API::API, feature_category: :system_access do
|
|||
end
|
||||
|
||||
describe 'Grape::Exceptions::Base handler' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it 'returns 400 on JSON parse errors' do
|
||||
post api('/projects'),
|
||||
post api('/projects', user),
|
||||
params: '{"test":"random_\$escaped/symbols\;here"}',
|
||||
headers: { 'content-type' => 'application/json' }
|
||||
|
||||
|
|
|
|||
|
|
@ -3451,10 +3451,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
|
|||
end.to change { shared_group.shared_with_group_links.count }.by(-1)
|
||||
end
|
||||
|
||||
it 'requires the group id to be an integer' do
|
||||
it 'returns a 404 error when the group id is not an integer' do
|
||||
delete api("/groups/#{shared_group.id}/share/foo", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns a 404 error when group link does not exist' do
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces
|
|||
env['warden'] = warden
|
||||
end
|
||||
|
||||
def error!(message, status, header)
|
||||
def error!(message, status, header = {})
|
||||
raise StandardError, "#{status} - #{message}"
|
||||
end
|
||||
|
||||
|
|
@ -336,7 +336,7 @@ RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_acces
|
|||
|
||||
describe '.handle_api_exception' do
|
||||
before do
|
||||
allow_any_instance_of(self.class).to receive(:rack_response)
|
||||
allow_any_instance_of(self.class).to receive(:error!)
|
||||
|
||||
stub_sentry_settings
|
||||
|
||||
|
|
|
|||
|
|
@ -4016,10 +4016,10 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
|
|||
end
|
||||
end
|
||||
|
||||
it 'returns a 400 when group id is not an integer' do
|
||||
it 'returns a 404 when group id is not an integer' do
|
||||
delete api("/projects/#{project.id}/share/foo", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns a 404 error when group link does not exist' do
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ RSpec.describe API::Suggestions, feature_category: :code_review_workflow do
|
|||
|
||||
put api(url, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response).to eq({ 'error' => 'id is invalid' })
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response).to eq({ 'error' => '404 Not Found' })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -129,11 +129,11 @@ RSpec.describe API::Topics, :aggregate_failures, :with_current_organization, fea
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns 400 for invalid `id` parameter' do
|
||||
it 'returns 404 for invalid `id` parameter' do
|
||||
get api('/topics/invalid')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eql('id is invalid')
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['error']).to eql('404 Not Found')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -245,11 +245,11 @@ RSpec.describe API::Topics, :aggregate_failures, :with_current_organization, fea
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns 400 for invalid `id` parameter' do
|
||||
it 'returns 404 for invalid `id` parameter' do
|
||||
put api('/topics/invalid', admin, admin_mode: true), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eql('id is invalid')
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['error']).to eql('404 Not Found')
|
||||
end
|
||||
|
||||
context 'with blank avatar' do
|
||||
|
|
@ -318,11 +318,11 @@ RSpec.describe API::Topics, :aggregate_failures, :with_current_organization, fea
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns 400 for invalid `id` parameter' do
|
||||
it 'returns 404 for invalid `id` parameter' do
|
||||
delete api('/topics/invalid', admin, admin_mode: true), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eql('id is invalid')
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['error']).to eql('404 Not Found')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3865,10 +3865,10 @@ RSpec.describe API::Users, :with_current_organization, :aggregate_failures, feat
|
|||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it "returns 400 for invalid ID" do
|
||||
it "returns 404 for invalid ID" do
|
||||
delete api("/user/emails/ASDF", admin, admin_mode: true)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -627,7 +627,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
|
|||
response_status: 400
|
||||
).deep_stringify_keys
|
||||
),
|
||||
'failed',
|
||||
'error',
|
||||
''
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -116,28 +116,6 @@ RSpec.describe WebHooks::LogExecutionService, feature_category: :webhooks do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when response_category is :failed' do
|
||||
let(:response_category) { :failed }
|
||||
|
||||
before do
|
||||
data[:response_status] = '400'
|
||||
end
|
||||
|
||||
it 'increments the failure count' do
|
||||
expect { service.execute }.to change { project_hook.recent_failures }.by(1)
|
||||
end
|
||||
|
||||
it 'does not change the disabled_until attribute' do
|
||||
expect { service.execute }.not_to change { project_hook.disabled_until }
|
||||
end
|
||||
|
||||
it 'does not allow the failure count to overflow' do
|
||||
project_hook.update!(recent_failures: 32767)
|
||||
|
||||
expect { service.execute }.not_to change { project_hook.recent_failures }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response_category is :error' do
|
||||
let(:response_category) { :error }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'with webhook auto-disabling failure thresholds' do
|
||||
where(:recent_failures, :disabled_until, :executable) do
|
||||
past = 1.minute.ago
|
||||
now = Time.current
|
||||
future = 1.minute.from_now
|
||||
|
||||
[
|
||||
# At 3 failures the hook is always executable
|
||||
[3, nil, true],
|
||||
[3, past, true],
|
||||
[3, now, true],
|
||||
[3, future, true],
|
||||
# At 4 failures the hook is executable only when disabled_until is in the past
|
||||
[4, nil, false],
|
||||
[4, past, true],
|
||||
[4, now, true],
|
||||
[4, future, false],
|
||||
# At 39 failures the logic should be the same as with 4 failures (testing the boundary of 40)
|
||||
[39, nil, false],
|
||||
[39, past, true],
|
||||
[39, now, true],
|
||||
[39, future, false],
|
||||
# At 40 failures the hook is always disabled
|
||||
[40, nil, false],
|
||||
[40, past, false],
|
||||
[40, now, false],
|
||||
[40, future, false]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -8,144 +8,64 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
allow(logger).to receive(:info)
|
||||
end
|
||||
|
||||
shared_examples 'is tolerant of invalid records' do
|
||||
specify do
|
||||
hook.url = nil
|
||||
describe '.executable and .disabled', :freeze_time do
|
||||
include_context 'with webhook auto-disabling failure thresholds'
|
||||
|
||||
expect(hook).to be_invalid
|
||||
run_expectation
|
||||
end
|
||||
end
|
||||
|
||||
describe '.executable/.disabled', :freeze_time do
|
||||
let!(:not_executable) do
|
||||
[
|
||||
[4, nil], # Exceeded the grace period, set by #fail!
|
||||
[4, 1.second.from_now], # Exceeded the grace period, set by #backoff!
|
||||
[4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case
|
||||
].map do |(recent_failures, disabled_until)|
|
||||
create(
|
||||
hook_factory,
|
||||
**default_factory_arguments,
|
||||
with_them do
|
||||
let(:web_hook) do
|
||||
factory_arguments = default_factory_arguments.merge(
|
||||
recent_failures: recent_failures,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:executables) do
|
||||
expired = 1.second.ago
|
||||
borderline = Time.current
|
||||
suspended = 1.second.from_now
|
||||
|
||||
[
|
||||
# Most of these are impossible states, but are included for completeness
|
||||
[0, nil],
|
||||
[1, nil],
|
||||
[3, nil],
|
||||
[4, expired],
|
||||
|
||||
# Impossible cases:
|
||||
[3, suspended],
|
||||
[3, expired],
|
||||
[3, borderline],
|
||||
[1, suspended],
|
||||
[1, expired],
|
||||
[1, borderline],
|
||||
[0, borderline],
|
||||
[0, suspended],
|
||||
[0, expired]
|
||||
].map do |(recent_failures, disabled_until)|
|
||||
create(
|
||||
hook_factory,
|
||||
**default_factory_arguments,
|
||||
recent_failures: recent_failures,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'finds the correct set of project hooks' do
|
||||
expect(find_hooks.executable).to match_array executables
|
||||
expect(find_hooks.executable).to all(be_executable)
|
||||
|
||||
# As expected, and consistent
|
||||
expect(find_hooks.disabled).to match_array not_executable
|
||||
expect(find_hooks.disabled.map(&:executable?)).not_to include(true)
|
||||
|
||||
# Nothing is missing
|
||||
expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
create(hook_factory, **factory_arguments)
|
||||
end
|
||||
|
||||
it 'causes all hooks to be considered executable' do
|
||||
expect(find_hooks.executable.count).to eq(16)
|
||||
it 'scopes correctly' do
|
||||
if executable
|
||||
expect(find_hooks.executable).to match_array([web_hook])
|
||||
expect(find_hooks.disabled).to be_empty
|
||||
else
|
||||
expect(find_hooks.executable).to be_empty
|
||||
expect(find_hooks.disabled).to match_array([web_hook])
|
||||
end
|
||||
end
|
||||
|
||||
it 'causes no hooks to be considered disabled' do
|
||||
expect(find_hooks.disabled).to be_empty
|
||||
end
|
||||
end
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
context 'when silent mode is enabled' do
|
||||
before do
|
||||
stub_application_setting(silent_mode_enabled: true)
|
||||
it 'causes all hooks to be scoped as executable' do
|
||||
expect(find_hooks.executable).to match_array([web_hook])
|
||||
expect(find_hooks.disabled).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it 'causes no hooks to be considered executable' do
|
||||
expect(find_hooks.executable).to be_empty
|
||||
end
|
||||
context 'when silent mode is enabled' do
|
||||
before do
|
||||
stub_application_setting(silent_mode_enabled: true)
|
||||
end
|
||||
|
||||
it 'causes all hooks to be considered disabled' do
|
||||
expect(find_hooks.disabled.count).to eq(16)
|
||||
it 'causes all hooks to be scoped as disabled' do
|
||||
expect(find_hooks.executable).to be_empty
|
||||
expect(find_hooks.disabled).to match_array([web_hook])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#executable?', :freeze_time do
|
||||
let(:web_hook) { create(hook_factory, **default_factory_arguments) }
|
||||
|
||||
where(:recent_failures, :not_until, :executable) do
|
||||
[
|
||||
[0, :not_set, true],
|
||||
[0, :past, true],
|
||||
[0, :future, true],
|
||||
[0, :now, true],
|
||||
[1, :not_set, true],
|
||||
[1, :past, true],
|
||||
[1, :future, true],
|
||||
[3, :not_set, true],
|
||||
[3, :past, true],
|
||||
[3, :future, true],
|
||||
[4, :not_set, false],
|
||||
[4, :past, true], # expired suspension
|
||||
[4, :now, false], # active suspension
|
||||
[4, :future, false] # active suspension
|
||||
]
|
||||
end
|
||||
include_context 'with webhook auto-disabling failure thresholds'
|
||||
|
||||
with_them do
|
||||
# Phasing means we cannot put these values in the where block,
|
||||
# which is not subject to the frozen time context.
|
||||
let(:disabled_until) do
|
||||
case not_until
|
||||
when :not_set
|
||||
nil
|
||||
when :past
|
||||
1.minute.ago
|
||||
when :future
|
||||
1.minute.from_now
|
||||
when :now
|
||||
Time.current
|
||||
end
|
||||
end
|
||||
let(:web_hook) do
|
||||
factory_arguments = default_factory_arguments.merge(
|
||||
recent_failures: recent_failures,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
|
||||
before do
|
||||
web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
|
||||
create(hook_factory, **factory_arguments)
|
||||
end
|
||||
|
||||
it 'has the correct state' do
|
||||
|
|
@ -164,24 +84,17 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#enable!' do
|
||||
it 'makes a hook executable if it was marked as failed' do
|
||||
hook.recent_failures = 1000
|
||||
|
||||
expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
|
||||
describe '#enable!', :freeze_time do
|
||||
before do
|
||||
hook.recent_failures = WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD
|
||||
hook.backoff!
|
||||
end
|
||||
|
||||
it 'makes a hook executable if it is currently backed off' do
|
||||
hook.recent_failures = 1000
|
||||
hook.disabled_until = 1.hour.from_now
|
||||
|
||||
it 'makes a hook executable' do
|
||||
expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
|
||||
end
|
||||
|
||||
it 'logs relevant information' do
|
||||
hook.recent_failures = 1000
|
||||
hook.disabled_until = 1.hour.from_now
|
||||
|
||||
expect(logger)
|
||||
.to receive(:info)
|
||||
.with(a_hash_including(
|
||||
|
|
@ -196,32 +109,50 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
end
|
||||
|
||||
it 'does not update hooks unless necessary' do
|
||||
hook
|
||||
hook.recent_failures = 0
|
||||
hook.backoff_count = 0
|
||||
hook.disabled_until = nil
|
||||
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
|
||||
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
|
||||
include_examples 'is tolerant of invalid records' do
|
||||
def run_expectation
|
||||
hook.recent_failures = 1000
|
||||
it 'is tolerant of invalid records' do
|
||||
hook.url = nil
|
||||
|
||||
expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
|
||||
end
|
||||
expect(hook).to be_invalid
|
||||
expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#backoff!', :freeze_time do
|
||||
describe '#backoff!' do
|
||||
around do |example|
|
||||
if example.metadata[:skip_freeze_time]
|
||||
example.run
|
||||
else
|
||||
freeze_time { example.run }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when we have not backed off before' do
|
||||
it 'does not disable the hook' do
|
||||
expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
|
||||
it 'increments recent_failures' do
|
||||
expect { hook.backoff! }.to change { hook.recent_failures }.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'does not increment backoff_count' do
|
||||
expect { hook.backoff! }.not_to change { hook.backoff_count }.from(0)
|
||||
end
|
||||
|
||||
it 'does not set disabled_until' do
|
||||
expect { hook.backoff! }.not_to change { hook.disabled_until }.from(nil)
|
||||
end
|
||||
|
||||
it 'logs relevant information' do
|
||||
expect(logger)
|
||||
.to receive(:info)
|
||||
|
|
@ -233,19 +164,29 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when we have exhausted the grace period' do
|
||||
context 'when failures exceed the threshold' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD)
|
||||
end
|
||||
|
||||
it 'disables the hook' do
|
||||
it 'temporarily disables the hook' do
|
||||
expect { hook.backoff! }.to change { hook.executable? }.from(true).to(false)
|
||||
expect(hook).to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
expect(hook.class.executable).not_to include(hook)
|
||||
end
|
||||
|
||||
it 'increments backoff_count' do
|
||||
expect { hook.backoff! }.to change { hook.backoff_count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'increments recent_failures' do
|
||||
expect { hook.backoff! }.to change {
|
||||
hook.recent_failures
|
||||
}.from(WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD)
|
||||
.to(WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it 'sets disabled_until' do
|
||||
expect { hook.backoff! }.to change { hook.disabled_until }.from(nil).to(1.minute.from_now)
|
||||
end
|
||||
|
|
@ -256,7 +197,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
.with(a_hash_including(
|
||||
hook_id: hook.id,
|
||||
action: 'backoff',
|
||||
recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1,
|
||||
recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1,
|
||||
disabled_until: 1.minute.from_now,
|
||||
backoff_count: 1
|
||||
))
|
||||
|
|
@ -264,23 +205,71 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
hook.backoff!
|
||||
end
|
||||
|
||||
it 'is no longer disabled after the backoff time has elapsed', :skip_freeze_time do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
expect(hook.class.executable).not_to include(hook)
|
||||
|
||||
travel_to(hook.disabled_until + 1.second) do
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
end
|
||||
|
||||
it 'increases the backoff time exponentially', :skip_freeze_time do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1),
|
||||
backoff_count: 1,
|
||||
disabled_until: be_like_time(Time.zone.now + 1.minute)
|
||||
)
|
||||
|
||||
travel_to(hook.disabled_until + 1.second) do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 2),
|
||||
backoff_count: 2,
|
||||
disabled_until: be_like_time(Time.zone.now + 2.minutes)
|
||||
)
|
||||
end
|
||||
|
||||
travel_to(hook.disabled_until + 1.second) do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 3),
|
||||
backoff_count: 3,
|
||||
disabled_until: be_like_time(Time.zone.now + 4.minutes)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the hook is permanently disabled' do
|
||||
before do
|
||||
allow(hook).to receive(:permanently_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not set disabled_until' do
|
||||
expect { hook.backoff! }.not_to change { hook.disabled_until }
|
||||
end
|
||||
it 'does not do anything' do
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.backoff! }.count
|
||||
|
||||
it 'does not increment the backoff count' do
|
||||
expect { hook.backoff! }.not_to change { hook.backoff_count }
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'is tolerant of invalid records' do
|
||||
def run_expectation
|
||||
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
|
||||
context 'when the hook is temporarily disabled' do
|
||||
before do
|
||||
allow(hook).to receive(:temporarily_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not do anything' do
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.backoff! }.count
|
||||
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -289,80 +278,66 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'does not increment backoff count' do
|
||||
expect { hook.failed! }.not_to change { hook.backoff_count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#failed!' do
|
||||
include_examples 'is tolerant of invalid records' do
|
||||
def run_expectation
|
||||
expect { hook.failed! }.to change { hook.recent_failures }.by(1)
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'does not increment recent failure count' do
|
||||
expect { hook.failed! }.not_to change { hook.recent_failures }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#temporarily_disabled?' do
|
||||
it 'is false when not temporarily disabled' do
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
|
||||
it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
|
||||
hook.backoff!
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
|
||||
hook.backoff!
|
||||
expect(hook).to be_temporarily_disabled
|
||||
end
|
||||
|
||||
context 'when hook has been told to back off' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
|
||||
hook.backoff!
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
expect(hook).to be_temporarily_disabled
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
it 'does not disable the hook' do
|
||||
expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
end
|
||||
|
||||
it 'is tolerant of invalid records' do
|
||||
hook.url = nil
|
||||
|
||||
expect(hook).to be_invalid
|
||||
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#permanently_disabled?' do
|
||||
it 'is false when not disabled' do
|
||||
describe '#temporarily_disabled? and #permanently_disabled?', :freeze_time do
|
||||
it 'is initially not disabled at all' do
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
|
||||
context 'when hook has been disabled' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
it 'becomes temporarily disabled after a threshold of failures has been exceeded' do
|
||||
WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD.times do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'is not disabled at all' do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD)
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook exceeds the permanently disabled threshold' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::PERMANENTLY_DISABLED_FAILURE_THRESHOLD)
|
||||
hook.backoff!
|
||||
end
|
||||
|
||||
it 'is permanently disabled' do
|
||||
expect(hook).to be_permanently_disabled
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
|
|
@ -370,24 +345,48 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
it 'is not disabled at all' do
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/525446
|
||||
context 'when hook has no disabled_until set and exceeds TEMPORARILY_DISABLED_FAILURE_THRESHOLD (legacy state)' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it 'is permanently disabled' do
|
||||
expect(hook).to be_permanently_disabled
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_disabling_web_hooks: false)
|
||||
end
|
||||
|
||||
it 'is not disabled at all' do
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#alert_status' do
|
||||
subject(:status) { hook.alert_status }
|
||||
|
||||
it { is_expected.to eq :executable }
|
||||
it { is_expected.to eq(:executable) }
|
||||
|
||||
context 'when hook has been disabled' do
|
||||
context 'when hook has been permanently disabled' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
allow(hook).to receive(:permanently_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq :disabled }
|
||||
it { is_expected.to eq(:disabled) }
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
|
|
@ -398,13 +397,12 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when hook has been backed off' do
|
||||
context 'when hook has been temporarily disabled' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
hook.disabled_until = 1.hour.from_now
|
||||
allow(hook).to receive(:temporarily_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to eq :temporarily_disabled }
|
||||
it { is_expected.to eq(:temporarily_disabled) }
|
||||
|
||||
context 'when the flag is disabled' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -1,97 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a hook that does not get automatically disabled on failure' do
|
||||
let(:exeeded_failure_threshold) { WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1 }
|
||||
|
||||
describe '.executable/.disabled', :freeze_time do
|
||||
let(:attributes_for_webhooks) do
|
||||
attributes_list = attributes_for_list(hook_factory, 13)
|
||||
include_context 'with webhook auto-disabling failure thresholds'
|
||||
|
||||
merged_attributes = attributes_list.zip([
|
||||
[0, Time.current],
|
||||
[0, 1.minute.from_now],
|
||||
[1, 1.minute.from_now],
|
||||
[3, 1.minute.from_now],
|
||||
[4, nil],
|
||||
[4, 1.day.ago],
|
||||
[4, 1.minute.from_now],
|
||||
[0, nil],
|
||||
[0, 1.day.ago],
|
||||
[1, nil],
|
||||
[1, 1.day.ago],
|
||||
[3, nil],
|
||||
[3, 1.day.ago]
|
||||
])
|
||||
with_them do
|
||||
let(:web_hook) do
|
||||
factory_arguments = default_factory_arguments.merge(
|
||||
recent_failures: recent_failures,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
|
||||
merged_attributes.map do |attributes, (recent_failures, disabled_until)|
|
||||
attributes.merge(**default_factory_arguments, recent_failures: recent_failures, disabled_until: disabled_until)
|
||||
end
|
||||
end
|
||||
|
||||
let(:webhooks) { described_class.create!(attributes_for_webhooks) }
|
||||
|
||||
it 'finds the correct set of project hooks' do
|
||||
expect(find_hooks).to all(be_executable)
|
||||
expect(find_hooks.executable).to match_array(webhooks)
|
||||
expect(find_hooks.disabled).to be_empty
|
||||
end
|
||||
|
||||
context 'when silent mode is enabled' do
|
||||
before do
|
||||
stub_application_setting(silent_mode_enabled: true)
|
||||
create(hook_factory, **factory_arguments)
|
||||
end
|
||||
|
||||
it 'causes no hooks to be considered executable' do
|
||||
expect(find_hooks.executable).to be_empty
|
||||
it 'is always enabled' do
|
||||
expect(find_hooks).to all(be_executable)
|
||||
expect(find_hooks.executable).to match_array(find_hooks)
|
||||
expect(find_hooks.disabled).to be_empty
|
||||
end
|
||||
|
||||
it 'causes all hooks to be considered disabled' do
|
||||
expect(find_hooks.disabled).to match_array(webhooks)
|
||||
context 'when silent mode is enabled' do
|
||||
before do
|
||||
stub_application_setting(silent_mode_enabled: true)
|
||||
end
|
||||
|
||||
it 'causes no hooks to be considered executable' do
|
||||
expect(find_hooks.executable).to be_empty
|
||||
end
|
||||
|
||||
it 'causes all hooks to be considered disabled' do
|
||||
expect(find_hooks.disabled).to match_array(find_hooks)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#executable?', :freeze_time do
|
||||
let(:web_hook) { build(hook_factory, **default_factory_arguments) }
|
||||
|
||||
where(:recent_failures, :not_until) do
|
||||
[
|
||||
[0, :not_set],
|
||||
[0, :past],
|
||||
[0, :future],
|
||||
[0, :now],
|
||||
[1, :not_set],
|
||||
[1, :past],
|
||||
[1, :future],
|
||||
[3, :not_set],
|
||||
[3, :past],
|
||||
[3, :future],
|
||||
[4, :not_set],
|
||||
[4, :past], # expired suspension
|
||||
[4, :now], # active suspension
|
||||
[4, :future] # active suspension
|
||||
]
|
||||
end
|
||||
include_context 'with webhook auto-disabling failure thresholds'
|
||||
|
||||
with_them do
|
||||
# Phasing means we cannot put these values in the where block,
|
||||
# which is not subject to the frozen time context.
|
||||
let(:disabled_until) do
|
||||
case not_until
|
||||
when :not_set
|
||||
nil
|
||||
when :past
|
||||
1.minute.ago
|
||||
when :future
|
||||
1.minute.from_now
|
||||
when :now
|
||||
Time.current
|
||||
end
|
||||
let(:web_hook) do
|
||||
factory_arguments = default_factory_arguments.merge(
|
||||
recent_failures: recent_failures,
|
||||
disabled_until: disabled_until
|
||||
)
|
||||
|
||||
build(hook_factory, **factory_arguments)
|
||||
end
|
||||
|
||||
before do
|
||||
web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
|
||||
end
|
||||
|
||||
it 'has the correct state' do
|
||||
it 'is always executable' do
|
||||
expect(web_hook).to be_executable
|
||||
end
|
||||
end
|
||||
|
|
@ -129,7 +89,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
|
|||
|
||||
context 'when we have exhausted the grace period' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD)
|
||||
end
|
||||
|
||||
it 'does not disable the hook' do
|
||||
|
|
@ -144,7 +104,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
|
|||
expect(hook).not_to be_temporarily_disabled
|
||||
|
||||
# Backing off
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
|
||||
WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD.times do
|
||||
hook.backoff!
|
||||
expect(hook).not_to be_temporarily_disabled
|
||||
end
|
||||
|
|
@ -159,7 +119,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
|
|||
# Initially
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: exeeded_failure_threshold)
|
||||
|
||||
expect(hook).not_to be_permanently_disabled
|
||||
end
|
||||
|
|
@ -172,7 +132,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
|
|||
|
||||
context 'when hook has been disabled' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: exeeded_failure_threshold)
|
||||
end
|
||||
|
||||
it { is_expected.to eq :executable }
|
||||
|
|
@ -180,7 +140,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
|
|||
|
||||
context 'when hook has been backed off' do
|
||||
before do
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: exeeded_failure_threshold)
|
||||
hook.disabled_until = 1.hour.from_now
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ RSpec.shared_examples 'something that has web-hooks' do
|
|||
context 'when there is a failed hook' do
|
||||
before do
|
||||
hook = create_hook
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
|
||||
hook.update!(recent_failures: WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'a webhook' do |factory:, auto_disabling: true|
|
||||
RSpec.shared_examples 'a webhook' do |factory:|
|
||||
include AfterNextHelpers
|
||||
|
||||
let(:hook) { build(factory) }
|
||||
|
|
@ -560,148 +560,4 @@ RSpec.shared_examples 'a webhook' do |factory:, auto_disabling: true|
|
|||
it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#backoff!', if: auto_disabling do
|
||||
context 'when we have not backed off before' do
|
||||
it 'increments the recent_failures count but does not disable the hook yet' do
|
||||
expect { hook.backoff! }.to change { hook.recent_failures }.to(1)
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is at the failure threshold' do
|
||||
before do
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! }
|
||||
end
|
||||
|
||||
it 'is not yet disabled' do
|
||||
expect(hook.class.executable).to include(hook)
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD,
|
||||
backoff_count: 0,
|
||||
disabled_until: nil
|
||||
)
|
||||
end
|
||||
|
||||
context 'when hook is next told to backoff' do
|
||||
before do
|
||||
hook.backoff!
|
||||
end
|
||||
|
||||
it 'causes the hook to become disabled for initial backoff period' do
|
||||
expect(hook.class.executable).not_to include(hook)
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1),
|
||||
backoff_count: 1,
|
||||
disabled_until: 1.minute.from_now
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the backoff time has elapsed', :skip_freeze_time do
|
||||
it 'is no longer disabled' do
|
||||
travel_to(hook.disabled_until + 1.minute) do
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the hook is next told to backoff' do
|
||||
it 'disables the hook again, increasing the backoff time exponentially' do
|
||||
travel_to(hook.disabled_until + 1.minute) do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook.class.executable).not_to include(hook)
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::FAILURE_THRESHOLD + 2),
|
||||
backoff_count: 2,
|
||||
disabled_until: 2.minutes.from_now
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not do anything if the hook is currently temporarily disabled' do
|
||||
allow(hook).to receive(:temporarily_disabled?).and_return(true)
|
||||
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.backoff! }.count
|
||||
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not do anything if the hook is currently permanently disabled' do
|
||||
allow(hook).to receive(:permanently_disabled?).and_return(true)
|
||||
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.backoff! }.count
|
||||
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
|
||||
context 'when the counter are above MAX_FAILURES' do
|
||||
let(:max_failures) { WebHooks::AutoDisabling::MAX_FAILURES }
|
||||
|
||||
before do
|
||||
hook.update!(
|
||||
recent_failures: (max_failures + 1),
|
||||
backoff_count: (max_failures + 1),
|
||||
disabled_until: 1.hour.ago
|
||||
)
|
||||
end
|
||||
|
||||
it 'reduces the counter to MAX_FAILURES' do
|
||||
hook.backoff!
|
||||
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: max_failures,
|
||||
backoff_count: max_failures
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#failed!', if: auto_disabling do
|
||||
it 'increments the recent_failures count but does not disable the hook yet' do
|
||||
expect { hook.failed! }.to change { hook.recent_failures }.to(1)
|
||||
expect(hook.class.executable).to include(hook)
|
||||
end
|
||||
|
||||
context 'when hook is at the failure threshold' do
|
||||
before do
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.failed! }
|
||||
end
|
||||
|
||||
it 'is not yet disabled' do
|
||||
expect(hook.class.executable).to include(hook)
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD,
|
||||
backoff_count: 0,
|
||||
disabled_until: nil
|
||||
)
|
||||
end
|
||||
|
||||
context 'when hook is next failed' do
|
||||
before do
|
||||
hook.failed!
|
||||
end
|
||||
|
||||
it 'causes the hook to become disabled' do
|
||||
expect(hook.class.executable).not_to include(hook)
|
||||
expect(hook).to have_attributes(
|
||||
recent_failures: (WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1),
|
||||
backoff_count: 0,
|
||||
disabled_until: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not do anything if recent_failures is at MAX_FAILURES' do
|
||||
hook.recent_failures = WebHooks::AutoDisabling::MAX_FAILURES
|
||||
|
||||
sql_count = ActiveRecord::QueryRecorder.new { hook.failed! }.count
|
||||
|
||||
expect(sql_count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
|
|||
|
||||
context 'the hook is disabled' do
|
||||
before do
|
||||
hook.update!(recent_failures: hook.class::EXCEEDED_FAILURE_THRESHOLD)
|
||||
hook.update!(recent_failures: hook.class::TEMPORARILY_DISABLED_FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it "has the correct alert status", :aggregate_failures do
|
||||
|
|
@ -237,7 +237,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
|
|||
|
||||
context 'the hook is backed-off' do
|
||||
before do
|
||||
WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! }
|
||||
WebHooks::AutoDisabling::TEMPORARILY_DISABLED_FAILURE_THRESHOLD.times { hook.backoff! }
|
||||
hook.backoff!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ RSpec.shared_examples 'rejecting protection rules request when handling rule ids
|
|||
let(:url) { "/projects/#{project_id}/#{path}" }
|
||||
|
||||
where(:project_id, :protection_rule_id, :status) do
|
||||
ref(:valid_project_id) | 'invalid' | :bad_request
|
||||
ref(:valid_project_id) | non_existing_record_id | :not_found
|
||||
ref(:valid_project_id) | 'invalid' | :not_found
|
||||
ref(:valid_project_id) | non_existing_record_id | :not_found
|
||||
ref(:other_project_id) | ref(:valid_protection_rule_id) | :not_found
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -190,59 +190,69 @@ RSpec.describe Tasks::Ci::JobTokensTask, :silence_stdout, feature_category: :per
|
|||
|
||||
def allowed_route_without_policies
|
||||
instance_double(Grape::Router::Route,
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true }
|
||||
options: {
|
||||
description: 'route description',
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true }
|
||||
}
|
||||
},
|
||||
request_method: 'GET',
|
||||
description: 'route description',
|
||||
origin: 'path/to/allowed_route_without_policies'
|
||||
)
|
||||
end
|
||||
|
||||
def allowed_route_with_invalid_policies
|
||||
instance_double(Grape::Router::Route,
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { job_token_policies: :invalid_policy }
|
||||
options: {
|
||||
description: 'route description',
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { job_token_policies: :invalid_policy }
|
||||
}
|
||||
},
|
||||
request_method: 'GET',
|
||||
description: 'route description',
|
||||
origin: 'path/to/allowed_route_with_invalid_policies'
|
||||
)
|
||||
end
|
||||
|
||||
def allowed_route_with_valid_policies
|
||||
instance_double(Grape::Router::Route,
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { job_token_policies: :read_packages }
|
||||
options: {
|
||||
description: 'route description',
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { job_token_policies: :read_packages }
|
||||
}
|
||||
},
|
||||
request_method: 'GET',
|
||||
description: 'route description',
|
||||
origin: 'path/to/allowed_route_with_valid_policies'
|
||||
)
|
||||
end
|
||||
|
||||
def allowed_route_with_skipped_policies
|
||||
instance_double(Grape::Router::Route,
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { skip_job_token_policies: true }
|
||||
options: {
|
||||
description: 'route description',
|
||||
settings: {
|
||||
authentication: { job_token_allowed: true },
|
||||
authorization: { skip_job_token_policies: true }
|
||||
}
|
||||
},
|
||||
request_method: 'GET',
|
||||
description: 'route description',
|
||||
origin: 'path/to/allowed_route_with_skipped_policies'
|
||||
)
|
||||
end
|
||||
|
||||
def not_allowed_route
|
||||
instance_double(Grape::Router::Route,
|
||||
settings: {
|
||||
authentication: { job_token_allowed: false },
|
||||
authorization: { job_token_policies: :read_packages }
|
||||
options: {
|
||||
description: 'route description',
|
||||
settings: {
|
||||
authentication: { job_token_allowed: false },
|
||||
authorization: { job_token_policies: :read_packages }
|
||||
}
|
||||
},
|
||||
request_method: 'GET',
|
||||
description: 'route description',
|
||||
origin: 'path/to/route'
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ RSpec.describe 'projects/hooks/edit' do
|
|||
it 'renders alert' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_text(s_('Webhooks|Webhook failed to connect'))
|
||||
expect(rendered).to have_text(s_('Webhooks|Webhook disabled'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ RSpec.describe 'projects/hooks/edit' do
|
|||
it 'renders alert' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_text(s_('Webhooks|Webhook fails to connect'))
|
||||
expect(rendered).to have_text(s_('Webhooks|Webhook temporarily disabled'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ RSpec.describe 'projects/hooks/index' do
|
|||
|
||||
expect(rendered).to have_css('.gl-heading-2', text: _('Webhooks'))
|
||||
expect(rendered).to have_text('Webhooks')
|
||||
expect(rendered).not_to have_css('.gl-badge', text: _('Disabled'))
|
||||
expect(rendered).not_to have_css('.gl-badge', text: s_('Webhooks|Failed to connect'))
|
||||
expect(rendered).not_to have_css('.gl-badge', text: s_('Webhooks|Fails to connect'))
|
||||
expect(rendered).not_to have_css('.gl-badge', text: s_('Webhooks|Rate limited'))
|
||||
expect(rendered).not_to have_css('.gl-badge', text: s_('Webhooks|Disabled'))
|
||||
expect(rendered).not_to have_css('.gl-badge', text: s_('Webhooks|Temporarily disabled'))
|
||||
end
|
||||
|
||||
context 'webhook is rate limited' do
|
||||
|
|
@ -29,10 +29,10 @@ RSpec.describe 'projects/hooks/index' do
|
|||
allow(existing_hook).to receive(:rate_limited?).and_return(true)
|
||||
end
|
||||
|
||||
it 'renders "Disabled" badge' do
|
||||
it 'renders "Rate limited" badge' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('.gl-badge', text: _('Disabled'))
|
||||
expect(rendered).to have_css('.gl-badge', text: _('Webhooks|Rate limited'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -41,10 +41,10 @@ RSpec.describe 'projects/hooks/index' do
|
|||
allow(existing_hook).to receive(:permanently_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'renders "Failed to connect" badge' do
|
||||
it 'renders "Disabled" badge' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('.gl-badge', text: s_('Webhooks|Failed to connect'))
|
||||
expect(rendered).to have_css('.gl-badge', text: s_('Webhooks|Disabled'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -53,10 +53,10 @@ RSpec.describe 'projects/hooks/index' do
|
|||
allow(existing_hook).to receive(:temporarily_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'renders "Fails to connect" badge' do
|
||||
it 'renders "Temporarily disabled" badge' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('.gl-badge', text: s_('Webhooks|Fails to connect'))
|
||||
expect(rendered).to have_css('.gl-badge', text: s_('Webhooks|Temporarily disabled'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ RSpec.describe MergeRequestCleanupRefsWorker, feature_category: :code_review_wor
|
|||
expect(cleanup_schedule.completed_at).to be_nil
|
||||
end
|
||||
|
||||
context "and cleanup schedule has already failed #{WebHooks::AutoDisabling::FAILURE_THRESHOLD} times" do
|
||||
let(:failed_count) { WebHooks::AutoDisabling::FAILURE_THRESHOLD }
|
||||
context "and cleanup schedule has already failed #{described_class::FAILURE_THRESHOLD} times" do
|
||||
let(:failed_count) { described_class::FAILURE_THRESHOLD }
|
||||
|
||||
it 'marks the cleanup schedule as failed and track the failure' do
|
||||
expect(cleanup_schedule.reload).to be_failed
|
||||
|
|
|
|||
Loading…
Reference in New Issue