diff --git a/Gemfile b/Gemfile index 68bb8c7d171..82e5bababc3 100644 --- a/Gemfile +++ b/Gemfile @@ -246,7 +246,7 @@ gem 're2', '~> 1.6.0' gem 'version_sorter', '~> 2.2.4' # Export Ruby Regex to Javascript -gem 'js_regex', '~> 3.7' +gem 'js_regex', '~> 3.8' # User agent parsing gem 'device_detector' diff --git a/Gemfile.checksum b/Gemfile.checksum index 8103a29a20d..5a7956df3d9 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -286,7 +286,7 @@ {"name":"jaro_winkler","version":"1.5.4","platform":"ruby","checksum":"50c3e83c5a9e8769c1cf5b73c8b51bb6eebbf8852a0ee53bf6ad6e4dc63414f9"}, {"name":"jira-ruby","version":"2.1.4","platform":"ruby","checksum":"4267c095cac8323b9eef3ba866eb28bb1388b7623a5abb60c1e7caf12d4adb9e"}, {"name":"jmespath","version":"1.6.1","platform":"ruby","checksum":"40ca83f4141bdd1e503db5485de68b84237183d84cf7a159fbeebcc6005adbd6"}, -{"name":"js_regex","version":"3.7.0","platform":"ruby","checksum":"b13fac68c4416d1a5f21c3bab8a71b4530f424b7c4ff9f46d8e62b895dc05975"}, +{"name":"js_regex","version":"3.8.0","platform":"ruby","checksum":"7934bcdd5a0e6d5af4a520288fd4684a02a472ae55831d9178ccaf82356344b5"}, {"name":"json","version":"2.5.1","platform":"java","checksum":"be284a0c4a9d0373e81b0d5dfe71ed5b18d0479f05970e60a77be89a2978ce6c"}, {"name":"json","version":"2.5.1","platform":"ruby","checksum":"918d8c41dacb7cfdbe0c7bbd6014a5372f0cf1c454ca150e9f4010fe80cc3153"}, {"name":"json-jwt","version":"1.15.3","platform":"ruby","checksum":"66db4f14e538a774c15502a5b5b26b1f3e7585481bbb96df490aa74b5c2d6110"}, diff --git a/Gemfile.lock b/Gemfile.lock index 30063886ff8..31ec54a5347 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -758,9 +758,9 @@ GEM multipart-post oauth (~> 0.5, >= 0.5.0) jmespath (1.6.1) - js_regex (3.7.0) + js_regex (3.8.0) character_set (~> 1.4) - regexp_parser (~> 2.1) + regexp_parser (~> 2.5) regexp_property_values (~> 1.0) json (2.5.1) json-jwt (1.15.3) @@ -1673,7 +1673,7 @@ DEPENDENCIES ipaddress (~> 0.8.3) ipynbdiff! jira-ruby (~> 2.1.4) - js_regex (~> 3.7) + js_regex (~> 3.8) json (~> 2.5.1) json_schemer (~> 0.2.18) jwt (~> 2.1.0) diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index 2806b785816..392dd63b089 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -85,10 +85,12 @@ export const billingPlanNames = { }; const INTEGRATION_TYPE_SLACK = 'slack'; +const INTEGRATION_TYPE_SLACK_APPLICATION = 'gitlab_slack_application'; const INTEGRATION_TYPE_MATTERMOST = 'mattermost'; export const placeholderForType = { [INTEGRATION_TYPE_SLACK]: __('#general, #development'), + [INTEGRATION_TYPE_SLACK_APPLICATION]: __('#general, #development'), [INTEGRATION_TYPE_MATTERMOST]: __('my-channel'), }; diff --git a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue index 67647cadf19..3820a87e5ad 100644 --- a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue @@ -23,7 +23,7 @@ export default { }, computed: { ...mapGetters(['isInheriting']), - placeholder() { + defaultPlaceholder() { return placeholderForType[this.type]; }, }, @@ -55,7 +55,7 @@ export default { v-if="event.field" v-model="event.field.value" :name="fieldName(event.field.name)" - :placeholder="placeholder" + :placeholder="event.field.placeholder || defaultPlaceholder" :readonly="isInheriting" /> diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml index 3345b3043b8..8c12399fdbb 100644 --- a/app/views/projects/_merge_request_merge_checks_settings.html.haml +++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml @@ -12,6 +12,7 @@ s_('ProjectSettings|Skipped pipelines are considered successful'), help_text: s_('ProjectSettings|Introduces the risk of merging changes that do not pass the pipeline.'), checkbox_options: { class: 'gl-pl-6' } + = render_if_exists 'projects/merge_request_merge_checks_status_checks', form: form, project: @project = form.gitlab_ui_checkbox_component :only_allow_merge_if_all_discussions_are_resolved, s_('ProjectSettings|All threads must be resolved'), checkbox_options: { data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' } } diff --git a/config/feature_flags/development/integration_slack_app_notifications.yml b/config/feature_flags/development/integration_slack_app_notifications.yml index d233194c6d4..4b9903b25c9 100644 --- a/config/feature_flags/development/integration_slack_app_notifications.yml +++ b/config/feature_flags/development/integration_slack_app_notifications.yml @@ -1,7 +1,7 @@ --- name: integration_slack_app_notifications introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98663 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381012 milestone: '15.5' type: development group: group::integrations diff --git a/config/initializers/database_query_analyzers.rb b/config/initializers/database_query_analyzers.rb index 2e73fbb79a2..fcfc75341df 100644 --- a/config/initializers/database_query_analyzers.rb +++ b/config/initializers/database_query_analyzers.rb @@ -1,15 +1,18 @@ # frozen_string_literal: true # Currently we register validator only for `dev` or `test` environment -Gitlab::Database::QueryAnalyzer.instance.hook! -Gitlab::Database::QueryAnalyzer.instance.all_analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics) -Gitlab::Database::QueryAnalyzer.instance.all_analyzers.append( - ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification -) +Gitlab::Database::QueryAnalyzer.instance.tap do |query_analyzer| + query_analyzer.hook! -if Gitlab.dev_or_test_env? - query_analyzer = ::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection - Gitlab::Database::QueryAnalyzer.instance.all_analyzers.append(query_analyzer) + query_analyzer.all_analyzers.tap do |analyzers| + analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics) + analyzers.append(::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification) + + if Gitlab.dev_or_test_env? + analyzers.append(::Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection) + analyzers.append(::Gitlab::Database::QueryAnalyzers::QueryRecorder) + end + end end Gitlab::Application.configure do |config| diff --git a/config/open_api.yml b/config/open_api.yml index cd0b64ba1fd..f45691ea9f5 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -26,6 +26,10 @@ metadata: description: Operations related to deploy tokens - name: deployments description: Operations related to deployments + - name: environments + description: Operations related to environments + - name: feature_flags_user_lists + description: Operations related to accessing GitLab feature flag user lists - name: features description: Operations related to managing Flipper-based feature flags - name: freeze_periods diff --git a/doc/api/environments.md b/doc/api/environments.md index 0f969ea4fd3..89b4bb6a1de 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -20,7 +20,7 @@ GET /projects/:id/environments | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | no | Return the environment with this name. Mutually exclusive with `search` | | `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name` | -| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping` or `stopped`. If no state value given, returns all environments. | +| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments. | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/environments?name=review%2Ffix-foo" @@ -279,7 +279,7 @@ Example response: } ``` -## Edit an existing environment +## Update an existing environment Updates an existing environment's name and/or `external_url`. diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index fc3b106aa18..d26c698af89 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -107,6 +107,33 @@ job: - make build ``` +#### Skip job if the branch is empty + +Use [`rules:changes:compare_to`](../yaml/index.md#ruleschangescompare_to) to avoid +running a job when the branch is empty, which saves CI/CD resources. Compare the +branch to the default branch, and if the branch: + +- Doesn't have changed files, the job doesn't run. +- Has changed files, the job runs. + +For example, in a project with `main` as the default branch: + +```yaml +job: + script: + - echo "This job only runs for branches that are not empty" + rules: + - if: $CI_COMMIT_BRANCH + changes: + compare_to: refs/heads/main + paths: + - '*' +``` + +The rule for this job compares all files and paths (`*`) in the current branch against +the default branch `main`. The rule matches and the job runs only when there are +changes to the files in the branch. + ### Complex rules You can use all `rules` keywords, like `if`, `changes`, and `exists`, in the same diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 47ad2655710..a32ba9751c4 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -1155,7 +1155,7 @@ that use the same cache key use the same cache, including in different pipelines If not set, the default key is `default`. All jobs with the `cache` keyword but no `cache:key` share the `default` cache. -Must be used with `cache: path`, or nothing is cached. +Must be used with `cache: paths`, or nothing is cached. **Keyword type**: Job keyword. You can use it only as part of a job or in the [`default` section](#default). @@ -1325,7 +1325,7 @@ rspec: Use `cache:when` to define when to save the cache, based on the status of the job. -Must be used with `cache: path`, or nothing is cached. +Must be used with `cache: paths`, or nothing is cached. **Keyword type**: Job keyword. You can use it only as part of a job or in the [`default` section](#default). @@ -1365,7 +1365,7 @@ Use the `pull` policy when you have many jobs executing in parallel that use the This policy speeds up job execution and reduces load on the cache server. You can use a job with the `push` policy to build the cache. -Must be used with `cache: path`, or nothing is cached. +Must be used with `cache: paths`, or nothing is cached. **Keyword type**: Job keyword. You can use it only as part of a job or in the [`default` section](#default). @@ -1773,7 +1773,7 @@ deploy: **Additional details**: - Enviroments created from this job definition are assigned a [tier](../environments/index.md#deployment-tier-of-environments) based on this value. -- Existing environments don't have their tier updated if this value is added later. Existing enviroments must have their tier updated via the [Environments API](../../api/environments.md#edit-an-existing-environment). +- Existing environments don't have their tier updated if this value is added later. Existing enviroments must have their tier updated via the [Environments API](../../api/environments.md#update-an-existing-environment). **Related topics**: diff --git a/lib/api/api.rb b/lib/api/api.rb index 38c20e80d14..35d96efec5c 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -176,6 +176,8 @@ module API mount ::API::DeployKeys mount ::API::DeployTokens mount ::API::Deployments + mount ::API::Environments + mount ::API::FeatureFlagsUserLists mount ::API::Features mount ::API::FreezePeriods mount ::API::Metadata @@ -225,13 +227,11 @@ module API mount ::API::DebianProjectPackages mount ::API::DependencyProxy mount ::API::Discussions - mount ::API::Environments mount ::API::ErrorTracking::ClientKeys mount ::API::ErrorTracking::Collector mount ::API::ErrorTracking::ProjectSettings mount ::API::Events mount ::API::FeatureFlags - mount ::API::FeatureFlagsUserLists mount ::API::Files mount ::API::GenericPackages mount ::API::Geo diff --git a/lib/api/entities/environment.rb b/lib/api/entities/environment.rb index 3b6ed94c3f1..dc9911d5acb 100644 --- a/lib/api/entities/environment.rb +++ b/lib/api/entities/environment.rb @@ -5,10 +5,10 @@ module API class Environment < Entities::EnvironmentBasic include RequestAwareEntity - expose :tier + expose :tier, documentation: { type: 'string', example: 'development' } expose :project, using: Entities::BasicProjectDetails expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } - expose :state + expose :state, documentation: { type: 'string', example: 'available' } end end end diff --git a/lib/api/entities/environment_basic.rb b/lib/api/entities/environment_basic.rb index d9894eac147..1b4a9371820 100644 --- a/lib/api/entities/environment_basic.rb +++ b/lib/api/entities/environment_basic.rb @@ -3,7 +3,12 @@ module API module Entities class EnvironmentBasic < Grape::Entity - expose :id, :name, :slug, :external_url, :created_at, :updated_at + expose :id, documentation: { type: 'integer', example: 1 } + expose :name, documentation: { type: 'string', example: 'deploy' } + expose :slug, documentation: { type: 'string', example: 'deploy' } + expose :external_url, documentation: { type: 'string', example: 'https://deploy.gitlab.example.com' } + expose :created_at, documentation: { type: 'dateTime', example: '2019-05-25T18:55:13.252Z' } + expose :updated_at, documentation: { type: 'dateTime', example: '2019-05-25T18:55:13.252Z' } end end end diff --git a/lib/api/entities/feature_flag/user_list.rb b/lib/api/entities/feature_flag/user_list.rb index bc8b12ea22e..efb3261658a 100644 --- a/lib/api/entities/feature_flag/user_list.rb +++ b/lib/api/entities/feature_flag/user_list.rb @@ -6,13 +6,13 @@ module API class UserList < Grape::Entity include RequestAwareEntity - expose :id - expose :iid - expose :project_id - expose :created_at - expose :updated_at - expose :name - expose :user_xids + expose :id, documentation: { type: 'integer', example: 1 } + expose :iid, documentation: { type: 'integer', example: 1 } + expose :project_id, documentation: { type: 'integer', example: 2 } + expose :created_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' } + expose :updated_at, documentation: { type: 'dateTime', example: '2020-02-04T08:13:10.507Z' } + expose :name, documentation: { type: 'string', example: 'user_list' } + expose :user_xids, documentation: { type: 'string', example: 'user1,user2' } expose :path do |list| project_feature_flags_user_list_path(list.project, list) diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 42d5e6a73b3..33f27f4dace 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -11,18 +11,27 @@ module API urgency :low params do - requires :id, type: String, desc: 'The project ID' + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project owned by the authenticated user' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Get all environments of the project' do - detail 'This feature was introduced in GitLab 8.11.' + desc 'List environments' do + detail 'Get all environments for a given project. This feature was introduced in GitLab 8.11.' success Entities::Environment + is_array true + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' } + ] + tags %w[environments] end params do use :pagination - optional :name, type: String, desc: 'Returns the environment with this name' - optional :search, type: String, desc: 'Returns list of environments matching the search criteria' - optional :states, type: String, values: Environment.valid_states.map(&:to_s), desc: 'List all environments that match a specific state' + optional :name, type: String, desc: 'Return the environment with this name. Mutually exclusive with search' + optional :search, type: String, desc: 'Return list of environments matching the search criteria. Mutually exclusive with name' + optional :states, + type: String, + values: Environment.valid_states.map(&:to_s), + desc: 'List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments' mutually_exclusive :name, :search, message: 'cannot be used together' end get ':id/environments' do @@ -33,15 +42,21 @@ module API present paginate(environments), with: Entities::Environment, current_user: current_user end - desc 'Creates a new environment' do - detail 'This feature was introduced in GitLab 8.11.' + desc 'Create a new environment' do + detail 'Creates a new environment with the given name and `external_url`. It returns `201` if the environment was successfully created, `400` for wrong parameters. This feature was introduced in GitLab 8.11.' success Entities::Environment + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' }, + { code: 422, message: 'Unprocessable entity' } + ] + tags %w[environments] end params do - requires :name, type: String, desc: 'The name of the environment to be created' - optional :external_url, type: String, desc: 'URL on which this deployment is viewable' + requires :name, type: String, desc: 'The name of the environment' + optional :external_url, type: String, desc: 'Place to link to for this environment' optional :slug, absence: { message: "is automatically generated and cannot be changed" }, documentation: { hidden: true } - optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the environment to be created' + optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`' end post ':id/environments' do authorize! :create_environment, user_project @@ -55,17 +70,23 @@ module API end end - desc 'Updates an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' + desc 'Update an existing environment' do + detail 'Updates an existing environment name and/or `external_url`. It returns `200` if the environment was successfully updated. In case of an error, a status code `400` is returned. This feature was introduced in GitLab 8.11.' success Entities::Environment + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' }, + { code: 422, message: 'Unprocessable entity' } + ] + tags %w[environments] end params do - requires :environment_id, type: Integer, desc: 'The environment ID' + requires :environment_id, type: Integer, desc: 'The ID of the environment' # TODO: disallow renaming via the API https://gitlab.com/gitlab-org/gitlab/-/issues/338897 optional :name, type: String, desc: 'DEPRECATED: Renaming environment can lead to errors, this will be removed in 15.0' optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' optional :slug, absence: { message: "is automatically generated and cannot be changed" }, documentation: { hidden: true } - optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the environment to be created' + optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`' end put ':id/environments/:environment_id' do authorize! :update_environment, user_project @@ -80,14 +101,21 @@ module API end end - desc "Delete multiple stopped review apps" do - detail "Remove multiple stopped review environments older than a specific age" + desc 'Delete multiple stopped review apps' do + detail 'It schedules for deletion multiple environments that have already been stopped and are in the review app folder. The actual deletion is performed after 1 week from the time of execution. By default, it only deletes environments 30 days or older. You can change this default using the `before` parameter.' success Entities::EnvironmentBasic + failure [ + { code: 400, message: 'Bad request' }, + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' }, + { code: 409, message: 'Conflict' } + ] + tags %w[environments] end params do - optional :before, type: Time, desc: "The timestamp before which environments can be deleted. Defaults to 30 days ago.", default: -> { 30.days.ago } - optional :limit, type: Integer, desc: "Maximum number of environments to delete. Defaults to 100.", default: 100, values: 1..1000 - optional :dry_run, type: Boolean, desc: "If set, perform a dry run where no actual deletions will be performed. Defaults to true.", default: true + optional :before, type: Time, desc: "The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)", default: -> { 30.days.ago } + optional :limit, type: Integer, desc: "Maximum number of environments to delete. Defaults to 100", default: 100, values: 1..1000 + optional :dry_run, type: Boolean, desc: "Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment", default: true end delete ":id/environments/review_apps" do authorize! :read_environment, user_project @@ -107,12 +135,17 @@ module API end end - desc 'Deletes an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' + desc 'Delete an environment' do + detail 'It returns 204 if the environment was successfully deleted, and 404 if the environment does not exist. This feature was introduced in GitLab 8.11.' success Entities::Environment + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' } + ] + tags %w[environments] end params do - requires :environment_id, type: Integer, desc: 'The environment ID' + requires :environment_id, type: Integer, desc: 'The ID of the environment' end delete ':id/environments/:environment_id' do authorize! :read_environment, user_project @@ -123,12 +156,18 @@ module API destroy_conditionally!(environment) end - desc 'Stops an existing environment' do + desc 'Stop an environment' do + detail 'It returns 200 if the environment was successfully stopped, and 404 if the environment does not exist.' success Entities::Environment + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' } + ] + tags %w[environments] end params do - requires :environment_id, type: Integer, desc: 'The environment ID' - optional :force, type: Boolean, default: false + requires :environment_id, type: Integer, desc: 'The ID of the environment' + optional :force, type: Boolean, default: false, desc: 'Force environment to stop without executing `on_stop` actions' end post ':id/environments/:environment_id/stop' do authorize! :read_environment, user_project @@ -141,11 +180,16 @@ module API present environment, with: Entities::Environment, current_user: current_user end - desc 'Get a single environment' do + desc 'Get a specific environment' do success Entities::Environment + failure [ + { code: 403, message: 'Unauthenticated' }, + { code: 404, message: 'Not found' } + ] + tags %w[environments] end params do - requires :environment_id, type: Integer, desc: 'The environment ID' + requires :environment_id, type: Integer, desc: 'The ID of the environment' end get ':id/environments/:environment_id' do authorize! :read_environment, user_project diff --git a/lib/api/feature_flags_user_lists.rb b/lib/api/feature_flags_user_lists.rb index f4771c07260..c5060eae763 100644 --- a/lib/api/feature_flags_user_lists.rb +++ b/lib/api/feature_flags_user_lists.rb @@ -16,16 +16,19 @@ module API end params do - requires :id, type: String, desc: 'The ID of a project' + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :feature_flags_user_lists do - desc 'Get all feature flags user lists of a project' do - detail 'This feature was introduced in GitLab 12.10' + desc 'List all feature flag user lists for a project' do + detail 'Gets all feature flag user lists for the requested project. ' \ + 'This feature was introduced in GitLab 12.10.' success ::API::Entities::FeatureFlag::UserList + is_array true + tags %w[feature_flags_user_lists] end params do - optional :search, type: String, desc: 'Returns the list of user lists matching the search critiera' + optional :search, type: String, desc: 'Return user lists matching the search criteria' use :pagination end @@ -35,9 +38,10 @@ module API with: ::API::Entities::FeatureFlag::UserList end - desc 'Create a feature flags user list for a project' do - detail 'This feature was introduced in GitLab 12.10' + desc 'Create a feature flag user list' do + detail 'Creates a feature flag user list. This feature was introduced in GitLab 12.10.' success ::API::Entities::FeatureFlag::UserList + tags %w[feature_flags_user_lists] end params do requires :name, type: String, desc: 'The name of the list' @@ -59,12 +63,13 @@ module API end params do - requires :iid, type: String, desc: 'The internal ID of the user list' + requires :iid, types: [String, Integer], desc: "The internal ID of the project's feature flag user list" end resource 'feature_flags_user_lists/:iid' do - desc 'Get a single feature flag user list belonging to a project' do - detail 'This feature was introduced in GitLab 12.10' + desc 'Get a feature flag user list' do + detail 'Gets a feature flag user list. This feature was introduced in GitLab 12.10.' success ::API::Entities::FeatureFlag::UserList + tags %w[feature_flags_user_lists] end get do present user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]), @@ -72,8 +77,9 @@ module API end desc 'Update a feature flag user list' do - detail 'This feature was introduced in GitLab 12.10' + detail 'Updates a feature flag user list. This feature was introduced in GitLab 12.10.' success ::API::Entities::FeatureFlag::UserList + tags %w[feature_flags_user_lists] end params do optional :name, type: String, desc: 'The name of the list' @@ -93,8 +99,9 @@ module API end end - desc 'Delete a feature flag user list' do - detail 'This feature was introduced in GitLab 12.10' + desc 'Delete feature flag user list' do + detail 'Deletes a feature flag user list. This feature was introduced in GitLab 12.10.' + tags %w[feature_flags_user_lists] end delete do # TODO: Move the business logic to a service class in app/services/feature_flags. diff --git a/lib/gitlab/database/query_analyzer.rb b/lib/gitlab/database/query_analyzer.rb index 6f64d04270f..1280789b30c 100644 --- a/lib/gitlab/database/query_analyzer.rb +++ b/lib/gitlab/database/query_analyzer.rb @@ -86,7 +86,11 @@ module Gitlab analyzers.each do |analyzer| next if analyzer.suppressed? && !analyzer.requires_tracking?(parsed) - analyzer.analyze(parsed) + if analyzer.raw? + analyzer.analyze(sql) + else + analyzer.analyze(parsed) + end rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e # We catch all standard errors to prevent validation errors to introduce fatal errors in production Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) diff --git a/lib/gitlab/database/query_analyzers/base.rb b/lib/gitlab/database/query_analyzers/base.rb index 9a52a4f6e23..9c2c228f869 100644 --- a/lib/gitlab/database/query_analyzers/base.rb +++ b/lib/gitlab/database/query_analyzers/base.rb @@ -53,6 +53,10 @@ module Gitlab Thread.current[self.context_key] end + def self.raw? + false + end + def self.enabled? raise NotImplementedError end diff --git a/lib/gitlab/database/query_analyzers/query_recorder.rb b/lib/gitlab/database/query_analyzers/query_recorder.rb new file mode 100644 index 00000000000..88fe829c3d2 --- /dev/null +++ b/lib/gitlab/database/query_analyzers/query_recorder.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module QueryAnalyzers + class QueryRecorder < Base + LOG_FILE = 'rspec/query_recorder.ndjson' + + class << self + def raw? + true + end + + def enabled? + # Only enable QueryRecorder in CI + ENV['CI'].present? + end + + def analyze(sql) + payload = { + sql: sql + } + + log_query(payload) + end + + private + + def log_query(payload) + log_path = Rails.root.join(LOG_FILE) + log_dir = File.dirname(log_path) + + # Create log directory if it does not exist since it is only created + # ahead of time by certain CI jobs + FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir) + + log_line = "#{Gitlab::Json.dump(payload)}\n" + + File.write(log_path, log_line, mode: 'a') + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e86ce08a1ce..4d73fcd48fa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4548,9 +4548,6 @@ msgstr "" msgid "Analytics" msgstr "" -msgid "AnalyticsDashboards|Dashboards" -msgstr "" - msgid "Analyze your dependencies for known vulnerabilities." msgstr "" @@ -25468,6 +25465,9 @@ msgstr "" msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgstr "" +msgid "Merge requests can't be merged if the status checks did not succeed or are still running." +msgstr "" + msgid "Merge the feature branch into the target branch and fix any conflicts. %{linkStart}How do I fix them?%{linkEnd}" msgstr "" @@ -32060,6 +32060,9 @@ msgstr "" msgid "ProjectSettings|Squashing is never performed and the checkbox is hidden." msgstr "" +msgid "ProjectSettings|Status checks must succeed" +msgstr "" + msgid "ProjectSettings|Submit changes to be merged upstream." msgstr "" diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js index c329ca8522f..082eeea30f1 100644 --- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js @@ -1,5 +1,6 @@ import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { placeholderForType } from 'jh_else_ce/integrations/constants'; import TriggerFields from '~/integrations/edit/components/trigger_fields.vue'; @@ -28,6 +29,50 @@ describe('TriggerFields', () => { const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput); + describe('placeholder text on the event fields and default values', () => { + const dummyFieldPlaceholder = '#foo'; + const integrationTypes = { + INTEGRATION_TYPE_SLACK: 'slack', + INTEGRATION_TYPE_SLACK_APPLICATION: 'gitlab_slack_application', + INTEGRATION_TYPE_MATTERMOST: 'mattermost', + INTEGRATION_TYPE_NON_EXISTING: 'non_existing', + }; + it.each` + integrationType | fieldPlaceholder | expectedPlaceholder + ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]} + ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]} + ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder} + ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]} + ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]} + ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder} + ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]} + ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]} + ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder} + ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${undefined} | ${undefined} + ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${''} | ${undefined} + ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder} + `( + 'passed down correct placeholder for "$integrationType" type and "$fieldPlaceholder" placeholder on the field', + ({ integrationType, fieldPlaceholder, expectedPlaceholder }) => { + createComponent({ + type: integrationType, + events: [ + { + field: { + name: 'foo', + value: '', + placeholder: fieldPlaceholder, + }, + }, + ], + }); + const field = wrapper.findComponent(GlFormInput); + + expect(field.attributes('placeholder')).toBe(expectedPlaceholder); + }, + ); + }); + describe.each([true, false])('template, isInheriting = `%p`', (isInheriting) => { it('renders a label with text "Trigger"', () => { createComponent(); diff --git a/spec/lib/gitlab/database/query_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzer_spec.rb index 0b849063562..6dc9ffc4aba 100644 --- a/spec/lib/gitlab/database/query_analyzer_spec.rb +++ b/spec/lib/gitlab/database/query_analyzer_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do before do allow(described_class.instance).to receive(:all_analyzers).and_return([analyzer, disabled_analyzer]) allow(analyzer).to receive(:enabled?).and_return(true) + allow(analyzer).to receive(:raw?).and_return(false) allow(analyzer).to receive(:suppressed?).and_return(false) allow(analyzer).to receive(:begin!) allow(analyzer).to receive(:end!) @@ -181,6 +182,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error end + it 'does call analyze with raw sql when raw? is true' do + expect(analyzer).to receive(:raw?).and_return(true) + expect(analyzer).to receive(:analyze).with('SELECT 1 FROM projects') + + expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error + end + def process_sql(sql) described_class.instance.within do ApplicationRecord.load_balancer.read_write do |connection| diff --git a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb new file mode 100644 index 00000000000..ec01ae623ae --- /dev/null +++ b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::QueryAnalyzers::QueryRecorder, query_analyzers: false do + # We keep only the QueryRecorder analyzer running + around do |example| + described_class.with_suppressed(false) do + example.run + end + end + + context 'when analyzer is enabled for tests' do + let(:query) { 'SELECT 1 FROM projects' } + let(:log_path) { Rails.root.join(described_class::LOG_FILE) } + + before do + stub_env('CI', 'true') + + # This is needed to be able to stub_env the CI variable + ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class]) + end + + after do + ::Gitlab::Database::QueryAnalyzer.instance.end!([described_class]) + end + + it 'logs queries to a file' do + allow(FileUtils).to receive(:mkdir_p) + .with(File.dirname(log_path)) + expect(File).to receive(:write) + .with(log_path, /^{"sql":"#{query}/, mode: 'a') + expect(described_class).to receive(:analyze).with(/^#{query}/).and_call_original + + expect { ApplicationRecord.connection.execute(query) }.not_to raise_error + end + end +end diff --git a/spec/support/database/query_recorder.rb b/spec/support/database/query_recorder.rb new file mode 100644 index 00000000000..1050120e528 --- /dev/null +++ b/spec/support/database/query_recorder.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + # Truncate the query_recorder log file before starting the suite + config.before(:suite) do + log_path = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder::LOG_FILE) + File.write(log_path, '') if File.exist?(log_path) + end +end