Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-27 12:07:11 +00:00
parent b7c55d3637
commit d044a84456
77 changed files with 832 additions and 698 deletions

View File

@ -1 +1 @@
4f276256d6ef947d1b8b12671dd6878f9fc513c3
1841bc63148c1743d4334f8dbeffbd54dee24d83

View File

@ -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

View File

@ -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"},

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -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

View File

@ -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}`;
}

View File

@ -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)"

View File

@ -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')"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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))

View File

@ -51,6 +51,7 @@ if Gitlab.ee?
Search::Zoekt::Task,
Ai::CodeSuggestionEvent,
Ai::DuoChatEvent,
Ai::TroubleshootJobEvent,
Vulnerabilities::Archive,
Vulnerabilities::ArchivedRecord,
Vulnerabilities::ArchiveExport

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
8ff13419285a7855b6a75e69ba63f10eb3ecb22655df946bb344d05201f7b6f6

View File

@ -0,0 +1 @@
43a806f0236fcf8d57242c339a90e1afbb7c1ca5950d29423da5648eb4b855ff

View File

@ -0,0 +1 @@
b90e017fcfdb70ab0d478f0d5fa2803fbcd6ee444c157902b34cab495496684b

View File

@ -0,0 +1 @@
93b32fdd10b4eddaad779c8aa8ae8f0a9f0806549ee9539659fefa09e79a87ad

View File

@ -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;

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
![Badges on failing webhooks](img/failed_badges_v14_9.png)
![Badges on failing webhooks](img/failed_badges_v17_11.png)
#### 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -114,7 +114,7 @@ module API
params do
use :terraform_get
end
get format: false do
get format: true do
present_package_file
end

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -45,4 +45,4 @@ tracer:
enabled: false
# https://gitlab.com/gitlab-org/gitlab/-/issues/471172
gitlab_http_router:
enabled: true
enabled: false

View File

@ -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

View File

@ -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],

View File

@ -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

View File

@ -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' },
]);
});
});

View File

@ -12,6 +12,10 @@ export const mockAvailableNamespaces = [
mockGroupFactory('match1'),
mockGroupFactory('unrelated'),
mockGroupFactory('match2'),
mockGroupFactory('sortme'),
mockGroupFactory('sortmea'),
mockGroupFactory('sortmeaa'),
mockGroupFactory('sortmeab'),
];
export const mockNamespacesResponse = {

View File

@ -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);
});

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -627,7 +627,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
response_status: 400
).deep_stringify_keys
),
'failed',
'error',
''
)

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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