Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-17 09:09:53 +00:00
parent af43640403
commit 612bb6f624
32 changed files with 974 additions and 266 deletions

View File

@ -47,7 +47,6 @@
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- tmp/feature_flags/
- log/*.log
reports:
junit: junit_rspec.xml
@ -149,13 +148,16 @@ setup-test-env:
- .rails-job-base
- .setup-test-env-cache
- .rails:rules:code-backstage-qa
- .use-pg12
stage: prepare
variables:
GITLAB_TEST_EAGER_LOAD: "0"
SETUP_DB: "false"
script:
- run_timed_command "bundle exec ruby -I. -e 'require \"config/environment\"; TestEnv.init'"
- run_timed_command "scripts/setup-test-env"
- echo -e "\e[0Ksection_start:`date +%s`:gitaly-test-build[collapsed=true]\r\e[0KCompiling Gitaly binaries"
- run_timed_command "scripts/gitaly-test-build" # Do not use 'bundle exec' here
- echo -e "\e[0Ksection_end:`date +%s`:gitaly-test-build\r\e[0K"
artifacts:
expire_in: 7d
paths:
@ -237,6 +239,11 @@ static-analysis:
script:
- run_timed_command "retry yarn install --frozen-lockfile"
- scripts/static-analysis
artifacts:
expire_in: 31d
when: always
paths:
- tmp/feature_flags/
static-analysis as-if-foss:
extends:
@ -487,23 +494,7 @@ rspec:feature-flags:
- .coverage-base
- .rails:rules:rspec-feature-flags
stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
- setup-test-env
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
- memory-static
- memory-on-boot
needs: ["static-analysis"]
script:
- !reference [.minimal-bundle-install, script]
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then

View File

@ -932,14 +932,6 @@
- <<: *if-merge-request-title-run-all-rspec
when: always
.rails:rules:rspec-feature-flags:
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-2-hourly
allow_failure: true
- <<: *if-merge-request-title-run-all-rspec
.rails:rules:default-branch-schedule-nightly--code-backstage:
rules:
- <<: *if-default-branch-schedule-nightly
@ -954,6 +946,12 @@
- <<: *if-merge-request
changes: [".gitlab/ci/rails.gitlab-ci.yml"]
.rails:rules:rspec-feature-flags:
rules:
- <<: *if-not-ee
when: never
- changes: *code-backstage-patterns
#########################
# Static analysis rules #
#########################

View File

@ -50,10 +50,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :show do
real_time_feature_flag = :real_time_issue_sidebar
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:real_time_issue_sidebar, @project)
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
push_to_gon_attributes(:features, :real_time_issue_sidebar, real_time_enabled)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)

View File

@ -7,8 +7,8 @@
alert_data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path },
close_button_data: { testid: 'close-registration-enabled-callout' } do
.gl-alert-body
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\">".html_safe, anchorClose: '</a>'.html_safe }
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\" class=\"gl-link\">".html_safe, anchorClose: '</a>'.html_safe }
.gl-alert-actions
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-info btn-md gl-button' do
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-confirm btn-md gl-button' do
%span.gl-button-text
= _('View setting')

View File

@ -10,5 +10,5 @@
%div
= _('Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work.')
.gl-alert-actions
= link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link btn gl-button btn-info'
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link btn gl-button btn-default gl-ml-2'
= link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link btn gl-button btn-confirm'
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link btn gl-button btn-default gl-ml-3'

View File

@ -2,19 +2,23 @@
- title = local_assigns.fetch(:title, nil)
- variant = local_assigns.fetch(:variant, :info)
- dismissible = local_assigns.fetch(:dismissible, true)
- alert_class = local_assigns.fetch(:alert_class, nil)
- alert_data = local_assigns.fetch(:alert_data, nil)
- close_button_class = local_assigns.fetch(:close_button_class, nil)
- close_button_data = local_assigns.fetch(:close_button_data, nil)
- icon = icons[variant]
- alert_root_class = 'gl-alert-layout-limited' if fluid_layout
- alert_container_class = [container_class, @content_class] unless fluid_layout || local_assigns.fetch(:is_contained, false)
%div{ role: 'alert', class: ["gl-alert-#{variant}", alert_class], data: alert_data }
%div{ class: [container_class, @content_class, 'gl-px-0!'] }
.gl-alert
= sprite_icon(icon, size: 16, css_class: "gl-alert-icon#{' gl-alert-icon-no-title' if title.nil?}")
%button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, class: close_button_class, data: close_button_data }
%div{ role: 'alert', class: [alert_root_class, 'gl-alert-max-content', 'gl-alert', "gl-alert-#{variant}", alert_class], data: alert_data }
.gl-alert-container{ class: alert_container_class }
= sprite_icon(icon, size: 16, css_class: "gl-alert-icon#{' gl-alert-icon-no-title' if title.nil?}")
- if dismissible
%button.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon.js-close{ type: 'button', aria: { label: _('Dismiss') }, class: close_button_class, data: close_button_data }
= sprite_icon('close', size: 16)
.gl-alert-content{ role: 'alert' }
- if title
.gl-alert-title
%h4.gl-alert-title
= title
= yield

View File

@ -373,9 +373,13 @@ When adding a custom domain, users are required to prove they own it by
adding a GitLab-controlled verification code to the DNS records for that domain.
If your user base is private or otherwise trusted, you can disable the
verification requirement. Go to **Admin Area > Settings > Preferences** and
uncheck **Require users to prove ownership of custom domains** in the **Pages** section.
This setting is enabled by default.
verification requirement:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > Preferences**.
1. Expand **Pages**.
1. Clear the **Require users to prove ownership of custom domains** checkbox.
This setting is enabled by default.
### Let's Encrypt integration
@ -388,9 +392,11 @@ sites served under a custom domain.
To enable it, you must:
1. Choose an email address on which you want to receive notifications about expiring domains.
1. Go to your instance's **Admin Area > Settings > Preferences** and expand **Pages** settings.
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > Preferences**.
1. Expand **Pages**.
1. Enter the email address for receiving notifications and accept Let's Encrypt's Terms of Service as shown below.
1. Click **Save changes**.
1. Select **Save changes**.
![Let's Encrypt settings](img/lets_encrypt_integration_v12_1.png)
@ -442,11 +448,12 @@ The scope to use for authentication must match the GitLab Pages OAuth applicatio
pre-existing applications must modify the GitLab Pages OAuth application. Follow these steps to do
this:
1. Go to your instance's **Admin Area > Settings > Applications** and expand **GitLab Pages**
settings.
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > Applications**.
1. Expand **GitLab Pages**.
1. Clear the `api` scope's checkbox and select the desired scope's checkbox (for example,
`read_api`).
1. Click **Save changes**.
1. Select **Save changes**.
#### Disabling public access to all Pages websites
@ -460,9 +467,11 @@ This can be useful to preserve information published with Pages websites to the
of your instance only.
To do that:
1. Go to your instance's **Admin Area > Settings > Preferences** and expand **Pages** settings.
1. Check the **Disable public access to Pages sites** checkbox.
1. Click **Save changes**.
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > Preferences**.
1. Expand **Pages**.
1. Select the **Disable public access to Pages sites** checkbox.
1. Select **Save changes**.
WARNING:
For self-managed installations, all public websites remain private until they are
@ -635,12 +644,6 @@ Follow the steps below to configure the proxy listener of GitLab Pages.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
## Set maximum pages size
You can configure the maximum size of the unpacked archive per project in
**Admin Area > Settings > Preferences > Pages**, in **Maximum size of pages (MB)**.
The default is 100MB.
### Override maximum pages size per project or group **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16610) in GitLab 12.7.
@ -1256,9 +1259,14 @@ Upgrading to an [officially supported operating system](https://about.gitlab.com
### The requested scope is invalid, malformed, or unknown
This problem comes from the permissions of the GitLab Pages OAuth application. To fix it, go to
**Admin > Applications > GitLab Pages** and edit the application. Under **Scopes**, ensure that the
`api` scope is selected and save your changes.
This problem comes from the permissions of the GitLab Pages OAuth application. To fix it:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Applications > GitLab Pages**.
1. Edit the application.
1. Under **Scopes**, ensure that the `api` scope is selected.
1. Save your changes.
When running a [separate Pages server](#running-gitlab-pages-on-a-separate-server),
this setting needs to be configured on the main GitLab server.

View File

@ -443,9 +443,14 @@ are stored.
## Set maximum Pages size
The maximum size of the unpacked archive per project can be configured in
**Admin Area > Settings > Preferences > Pages**, in **Maximum size of pages (MB)**.
The default is 100MB.
The default for the maximum size of unpacked archives per project is 100 MB.
To change this value:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > Preferences**.
1. Expand **Pages**.
1. Update the value for **Maximum size of pages (MB)**.
## Backup

View File

@ -11,7 +11,7 @@ type: reference, api
> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-status-checks). **(ULTIMATE SELF)**
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-external-status-checks).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
@ -67,13 +67,6 @@ POST /projects/:id/merge_requests/:merge_request_iid/status_check_responses
NOTE:
`sha` must be the SHA at the `HEAD` of the merge request's source branch.
## Enable or disable status checks **(ULTIMATE SELF)**
Status checks are under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can enable it.
## Get project external status checks **(ULTIMATE)**
You can request information about a project's external status checks using the following endpoint:
@ -86,7 +79,7 @@ GET /projects/:id/external_status_checks
| Attribute | Type | Required | Description |
|---------------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `id` | integer | yes | ID of a project |
```json
[
@ -109,7 +102,7 @@ GET /projects/:id/external_status_checks
]
```
### Create external status check **(ULTIMATE)**
## Create external status check **(ULTIMATE)**
You can create a new external status check for a project using the following endpoint:
@ -117,14 +110,14 @@ You can create a new external status check for a project using the following end
POST /projects/:id/external_status_checks
```
| Attribute | Type | Required | Description |
|------------------------|----------------|----------|----------------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `name` | string | yes | Display name of status check |
| `external_url` | string | yes | URL of status check resource |
| `protected_branch_ids` | `array<Integer>` | no | The ids of protected branches to scope the rule by |
| Attribute | Type | Required | Description |
|------------------------|------------------|----------|------------------------------------------------|
| `id` | integer | yes | ID of a project |
| `name` | string | yes | Display name of status check |
| `external_url` | string | yes | URL of status check resource |
| `protected_branch_ids` | `array<Integer>` | no | IDs of protected branches to scope the rule by |
### Delete external status check **(ULTIMATE)**
## Delete external status check **(ULTIMATE)**
You can delete an external status check for a project using the following endpoint:
@ -132,12 +125,12 @@ You can delete an external status check for a project using the following endpoi
DELETE /projects/:id/external_status_checks/:check_id
```
| Attribute | Type | Required | Description |
|------------------------|----------------|----------|----------------------------------------------------|
| `rule_id` | integer | yes | The ID of an status check |
| `id` | integer | yes | The ID of a project |
| Attribute | Type | Required | Description |
|------------------------|----------------|----------|-----------------------|
| `rule_id` | integer | yes | ID of an status check |
| `id` | integer | yes | ID of a project |
### Update external status check **(ULTIMATE)**
## Update external status check **(ULTIMATE)**
You can update an existing external status check for a project using the following endpoint:
@ -145,18 +138,22 @@ You can update an existing external status check for a project using the followi
PUT /projects/:id/external_status_checks/:check_id
```
| Attribute | Type | Required | Description |
|------------------------|----------------|----------|----------------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `rule_id` | integer | yes | The ID of an external status check |
| `name` | string | no | Display name of status check |
| `external_url` | string | no | URL of external status check resource |
| `protected_branch_ids` | `array<Integer>` | no | The ids of protected branches to scope the rule by |
| Attribute | Type | Required | Description |
|------------------------|------------------|----------|------------------------------------------------|
| `id` | integer | yes | ID of a project |
| `rule_id` | integer | yes | ID of an external status check |
| `name` | string | no | Display name of status check |
| `external_url` | string | no | URL of external status check resource |
| `protected_branch_ids` | `array<Integer>` | no | IDs of protected branches to scope the rule by |
### Enable or disable External Project-level MR status checks **(ULTIMATE SELF)**
## Enable or disable external status checks **(ULTIMATE SELF)**
Enable or disable External Project-level MR status checks is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
External status checks are:
- Under development.
- Not ready for production use.
The feature is deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../user/feature_flags.md)
can enable it.
@ -178,24 +175,6 @@ Feature.disable(:ff_compliance_approval_gates)
Feature.disable(:ff_compliance_approval_gates, Project.find(<project id>))
```
To enable it:
```ruby
# For the instance
Feature.enable(:ff_compliance_approval_gates)
# For a single project
Feature.enable(:ff_compliance_approval_gates, Project.find(<project id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:ff_compliance_approval_gates)
# For a single project
Feature.disable(:ff_compliance_approval_gates, Project.find(<project id>)
```
## Related links
- [External status checks](../user/project/merge_requests/status_checks.md)
- [External status checks](../user/project/merge_requests/status_checks.md).

View File

@ -19,62 +19,60 @@ Use cache for dependencies, like packages you download from the internet.
Cache is stored where GitLab Runner is installed and uploaded to S3 if
[distributed cache is enabled](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching).
- You can define it per job by using the `cache:` keyword. Otherwise it is disabled.
- You can define it per job so that:
- Subsequent pipelines can use it.
- Subsequent jobs in the same pipeline can use it, if the dependencies are identical.
- You cannot share it between projects.
Use artifacts to pass intermediate build results between stages.
Artifacts are generated by a job, stored in GitLab, and can be downloaded.
- You can define artifacts per job. Subsequent jobs in later stages of the same
pipeline can use them.
- You can't use the artifacts in a different pipeline.
Artifacts expire after 30 days unless you define an [expiration time](../yaml/README.md#artifactsexpire_in).
Use [dependencies](../yaml/README.md#dependencies) to control which jobs fetch the artifacts.
Both artifacts and caches define their paths relative to the project directory, and
can't link to files outside it.
### Cache
- Define cache per job by using the `cache:` keyword. Otherwise it is disabled.
- Subsequent pipelines can use the cache.
- Subsequent jobs in the same pipeline can use the cache, if the dependencies are identical.
- Different projects cannot share the cache.
### Artifacts
- Define artifacts per job.
- Subsequent jobs in later stages of the same pipeline can use artifacts.
- Different projects cannot share artifacts.
Artifacts expire after 30 days unless you define an [expiration time](../yaml/README.md#artifactsexpire_in).
Use [dependencies](../yaml/README.md#dependencies) to control which jobs fetch the artifacts.
## Good caching practices
To ensure maximum availability of the cache, when you declare `cache` in your jobs,
use one or more of the following:
To ensure maximum availability of the cache, do one or more of the following:
- [Tag your runners](../runners/configure_runners.md#use-tags-to-limit-the-number-of-jobs-using-the-runner) and use the tag on jobs
that share their cache.
that share the cache.
- [Use runners that are only available to a particular project](../runners/runners_scope.md#prevent-a-specific-runner-from-being-enabled-for-other-projects).
- [Use a `key`](../yaml/README.md#cachekey) that fits your workflow (for example,
different caches on each branch). For that, you can take advantage of the
[predefined CI/CD variables](../variables/README.md#predefined-cicd-variables).
- [Use a `key`](../yaml/README.md#cachekey) that fits your workflow. For example,
you can configure a different cache for each branch.
For runners to work with caches efficiently, you must do one of the following:
- Use a single runner for all your jobs.
- Use multiple runners (in autoscale mode or not) that use
- Use multiple runners that have
[distributed caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching),
where the cache is stored in S3 buckets (like shared runners on GitLab.com).
- Use multiple runners (not in autoscale mode) of the same architecture that
share a common network-mounted directory (using NFS or something similar)
where the cache is stored.
Read about the [availability of the cache](#availability-of-the-cache)
to learn more about the internals and get a better idea how cache works.
where the cache is stored in S3 buckets. Shared runners on GitLab.com behave this way. These runners can be in autoscale mode,
but they don't have to be.
- Use multiple runners with the same architecture and have these runners
share a common network-mounted directory to store the cache. This directory should use NFS or something similar.
These runners must be in autoscale mode.
### Share caches between jobs in the same branch
Define a cache with the `key: ${CI_COMMIT_REF_SLUG}` so that jobs of each
branch always use the same cache:
To have jobs for each branch use the same cache, define a cache with the `key: ${CI_COMMIT_REF_SLUG}`:
```yaml
cache:
key: ${CI_COMMIT_REF_SLUG}
```
This configuration is safe from accidentally overwriting the cache, but merge requests
get slow first pipelines. The next time a new commit is pushed to the branch, the
This configuration prevents you from accidentally overwriting the cache. However, the
first pipeline for a merge request is slow. The next time a commit is pushed to the branch, the
cache is re-used and jobs run faster.
To enable per-job and per-branch caching:

View File

@ -121,8 +121,9 @@ repositories to secure, shared services, such as CI/CD.
Instance administrators can add public deploy keys:
1. Go to **Admin Area > Deploy Keys**.
1. Click on **New deploy key**.
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Deploy Keys**.
1. Select **New deploy key**.
Make sure your new key has a meaningful title, as it is the primary way for project
maintainers and owners to identify the correct public deploy key to add. For example,

View File

@ -120,7 +120,7 @@ module Gitlab
raise "storage #{storage.inspect} is missing a gitaly_address"
end
unless URI(address).scheme.in?(%w(tcp unix tls))
unless %w(tcp unix tls).include?(URI(address).scheme)
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix' or 'tls'"
end

View File

@ -52,7 +52,7 @@ module Gitlab
@legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
storage['path'] = Deprecated
@hash = storage.with_indifferent_access
@hash = ActiveSupport::HashWithIndifferentAccess.new(storage)
end
def gitaly_address

View File

@ -3,6 +3,8 @@
# Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
task gitlab_environment: :environment do
task :gitlab_environment do
Rake::Task[:environment].invoke unless ENV['SKIP_RAILS_ENV_IN_RAKE']
extend SystemCheck::Helpers
end

View File

@ -21752,10 +21752,7 @@ msgstr ""
msgid "NetworkPolicies|Namespace"
msgstr ""
msgid "NetworkPolicies|Network Policy"
msgstr ""
msgid "NetworkPolicies|Network policy"
msgid "NetworkPolicies|Network"
msgstr ""
msgid "NetworkPolicies|Network traffic"

View File

@ -56,7 +56,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.199.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "29.35.0",
"@gitlab/ui": "29.36.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.3-2",
"@rails/ujs": "6.1.3-2",

View File

@ -32,59 +32,79 @@ module RuboCop
# Returns true if the given node resides in app/finders or ee/app/finders.
def in_finder?(node)
in_directory?(node, 'finders')
in_app_directory?(node, 'finders')
end
# Returns true if the given node resides in app/models or ee/app/models.
def in_model?(node)
in_directory?(node, 'models')
in_app_directory?(node, 'models')
end
# Returns true if the given node resides in app/services or ee/app/services.
def in_service_class?(node)
in_directory?(node, 'services')
in_app_directory?(node, 'services')
end
# Returns true if the given node resides in app/presenters or
# ee/app/presenters.
def in_presenter?(node)
in_directory?(node, 'presenters')
in_app_directory?(node, 'presenters')
end
# Returns true if the given node resides in app/serializers or
# ee/app/serializers.
def in_serializer?(node)
in_directory?(node, 'serializers')
in_app_directory?(node, 'serializers')
end
# Returns true if the given node resides in app/workers or ee/app/workers.
def in_worker?(node)
in_directory?(node, 'workers')
in_app_directory?(node, 'workers')
end
# Returns true if the given node resides in app/controllers or
# ee/app/controllers.
def in_controller?(node)
in_directory?(node, 'controllers')
in_app_directory?(node, 'controllers')
end
# Returns true if the given node resides in app/graphql/types,
# ee/app/graphql/types, or ee/app/graphql/ee/types.
def in_graphql_types?(node)
in_app_directory?(node, 'graphql/types') || in_app_directory?(node, 'graphql/ee/types')
end
# Returns true if the given node resides in lib/api or ee/lib/api.
def in_api?(node)
in_lib_directory?(node, 'api')
end
# Returns true if the given node resides in spec or ee/spec.
def in_spec?(node)
file_path_for_node(node).start_with?(
File.join(ce_lib_directory, 'api'),
File.join(ee_lib_directory, 'api')
ce_spec_directory,
ee_spec_directory
)
end
# Returns `true` if the given AST node resides in the given directory,
# relative to app and/or ee/app.
def in_directory?(node, directory)
def in_app_directory?(node, directory)
file_path_for_node(node).start_with?(
File.join(ce_app_directory, directory),
File.join(ee_app_directory, directory)
)
end
# Returns `true` if the given AST node resides in the given directory,
# relative to lib and/or ee/lib.
def in_lib_directory?(node, directory)
file_path_for_node(node).start_with?(
File.join(ce_lib_directory, directory),
File.join(ee_lib_directory, directory)
)
end
# Returns the receiver name of a send node.
#
# For the AST node `(send (const nil? :Foo) ...)` this would return
@ -149,6 +169,14 @@ module RuboCop
File.join(rails_root, 'ee', 'lib')
end
def ce_spec_directory
File.join(rails_root, 'spec')
end
def ee_spec_directory
File.join(rails_root, 'ee', 'spec')
end
def rails_root
File.expand_path('..', __dir__)
end

View File

@ -0,0 +1,265 @@
# frozen_string_literal: true
require_relative '../../code_reuse_helpers'
module RuboCop
module Cop
module Gitlab
# This cop tracks the usage of feature flags among the codebase.
#
# The files set in `tmp/feature_flags/*.used` can then be used for verification purpose.
#
class MarkUsedFeatureFlags < RuboCop::Cop::Cop
include RuboCop::CodeReuseHelpers
FEATURE_METHODS = %i[enabled? disabled?].freeze
EXPERIMENTATION_METHODS = %i[active?].freeze
EXPERIMENT_METHODS = %i[
experiment
experiment_enabled?
push_frontend_experiment
].freeze
RUGGED_METHODS = %i[
use_rugged?
].freeze
WORKER_METHODS = %i[
data_consistency
deduplicate
].freeze
GRAPHQL_METHODS = %i[
field
].freeze
SELF_METHODS = %i[
push_frontend_feature_flag
limit_feature_flag=
].freeze + EXPERIMENT_METHODS + RUGGED_METHODS + WORKER_METHODS
RESTRICT_ON_SEND = FEATURE_METHODS + EXPERIMENTATION_METHODS + GRAPHQL_METHODS + SELF_METHODS
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS = [
File.expand_path("../../../config/metrics/aggregates/*.yml", __dir__),
File.expand_path("../../../lib/gitlab/usage_data_counters/known_events/*.yml", __dir__)
].freeze
DYNAMIC_FEATURE_FLAGS = [
:usage_data_static_site_editor_commits, # https://gitlab.com/gitlab-org/gitlab/-/issues/284082
:usage_data_static_site_editor_merge_requests # https://gitlab.com/gitlab-org/gitlab/-/issues/284083
].freeze
# Called before all on_... have been called
# When refining this method, always call `super`
def on_new_investigation
super
track_dynamic_feature_flags!
track_usage_data_counters_known_events!
end
def on_casgn(node)
_, lhs_name, rhs = *node
save_used_feature_flag(rhs.value) if lhs_name == :FEATURE_FLAG
end
def on_send(node)
return if in_spec?(node)
return unless trackable_flag?(node)
flag_arg = flag_arg(node)
flag_value = flag_value(node)
return unless flag_value
if flag_arg_is_str_or_sym?(node)
if caller_is_feature_gitaly?(node)
save_used_feature_flag("gitaly_#{flag_value}")
else
save_used_feature_flag(flag_value)
end
if experiment_method?(node) || experimentation_method?(node)
# Additionally, mark experiment-related feature flag as used as well
matching_feature_flags = defined_feature_flags.select { |flag| flag == "#{flag_value}_experiment_percentage" }
matching_feature_flags.each do |matching_feature_flag|
puts_if_ci(node, "The '#{matching_feature_flag}' feature flag tracks the #{flag_value} experiment, which is still in use, so we'll mark it as used.")
save_used_feature_flag(matching_feature_flag)
end
end
elsif flag_arg_is_send_type?(node)
puts_if_ci(node, "Feature flag is dynamic: '#{flag_value}.")
elsif flag_arg_is_dstr_or_dsym?(node)
str_prefix = flag_arg.children[0]
rest_children = flag_arg.children[1..]
if rest_children.none? { |child| child.str_type? }
matching_feature_flags = defined_feature_flags.select { |flag| flag.start_with?(str_prefix.value) }
matching_feature_flags.each do |matching_feature_flag|
puts_if_ci(node, "The '#{matching_feature_flag}' feature flag starts with '#{str_prefix.value}', so we'll optimistically mark it as used.")
save_used_feature_flag(matching_feature_flag)
end
else
puts_if_ci(node, "Interpolated feature flag name has multiple static string parts, we won't track it.")
end
else
puts_if_ci(node, "Feature flag has an unknown type: #{flag_arg.type}.")
end
end
private
def puts_if_ci(node, text)
puts "#{text} (call: `#{node.source}`, source: #{node.location.expression.source_buffer.name})" if ENV['CI']
end
def save_used_feature_flag(feature_flag_name)
used_feature_flag_file = File.expand_path("../../../tmp/feature_flags/#{feature_flag_name}.used", __dir__)
return if File.exist?(used_feature_flag_file)
FileUtils.touch(used_feature_flag_file)
end
def class_caller(node)
node.children[0]&.const_name.to_s
end
def method_name(node)
node.children[1]
end
def flag_arg(node)
if worker_method?(node)
return unless node.children.size > 3
node.children[3].each_pair.find do |pair|
pair.key.value == :feature_flag
end&.value
elsif graphql_method?(node)
return unless node.children.size > 3
opts_index = node.children[3].hash_type? ? 3 : 4
return unless node.children[opts_index]
node.children[opts_index].each_pair.find do |pair|
pair.key.value == :feature_flag
end&.value
else
arg_index = rugged_method?(node) ? 3 : 2
node.children[arg_index]
end
end
def flag_value(node)
flag_arg = flag_arg(node)
return unless flag_arg
if flag_arg.respond_to?(:value)
flag_arg.value
else
flag_arg
end.to_s.tr("\n/", ' _')
end
def flag_arg_is_str_or_sym?(node)
flag_arg = flag_arg(node)
flag_arg.str_type? || flag_arg.sym_type?
end
def flag_arg_is_send_type?(node)
flag_arg(node).send_type?
end
def flag_arg_is_dstr_or_dsym?(node)
flag = flag_arg(node)
(flag.dstr_type? || flag.dsym_type?) && flag.children[0].str_type?
end
def caller_is_feature?(node)
class_caller(node) == "Feature"
end
def caller_is_feature_gitaly?(node)
class_caller(node) == "Feature::Gitaly"
end
def caller_is_experimentation?(node)
class_caller(node) == "Gitlab::Experimentation"
end
def experiment_method?(node)
EXPERIMENT_METHODS.include?(method_name(node))
end
def rugged_method?(node)
RUGGED_METHODS.include?(method_name(node))
end
def feature_method?(node)
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node))
end
def experimentation_method?(node)
EXPERIMENTATION_METHODS.include?(method_name(node)) && caller_is_experimentation?(node)
end
def worker_method?(node)
WORKER_METHODS.include?(method_name(node))
end
def graphql_method?(node)
GRAPHQL_METHODS.include?(method_name(node)) && in_graphql_types?(node)
end
def self_method?(node)
SELF_METHODS.include?(method_name(node)) && class_caller(node).empty?
end
def trackable_flag?(node)
feature_method?(node) || experimentation_method?(node) || graphql_method?(node) || self_method?(node)
end
# Marking all event's feature flags as used as Gitlab::UsageDataCounters::HLLRedisCounter.track_event{,context}
# is mostly used with dynamic event name.
def track_dynamic_feature_flags!
DYNAMIC_FEATURE_FLAGS.each(&method(:save_used_feature_flag))
end
# Marking all event's feature flags as used as Gitlab::UsageDataCounters::HLLRedisCounter.track_event{,context}
# is mostly used with dynamic event name.
def track_usage_data_counters_known_events!
usage_data_counters_known_event_feature_flags.each(&method(:save_used_feature_flag))
end
def usage_data_counters_known_event_feature_flags
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS.each_with_object(Set.new) do |glob, memo|
Dir.glob(glob).each do |path|
YAML.safe_load(File.read(path))&.each do |hash|
memo << hash['feature_flag'] if hash['feature_flag']
end
end
end
end
def defined_feature_flags
@defined_feature_flags ||= begin
flags_paths = [
'config/feature_flags/**/*.yml'
]
# For EE additionally process `ee/` feature flags
if File.exist?(File.expand_path('../../../ee/app/models/license.rb', __dir__)) && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
flags_paths << 'ee/config/feature_flags/**/*.yml'
end
flags_paths.each_with_object([]) do |flags_path, memo|
flags_path = File.expand_path("../../../#{flags_path}", __dir__)
Dir.glob(flags_path).each do |path|
feature_flag_name = File.basename(path, '.yml')
memo << feature_flag_name
end
end
end
end
end
end
end
end

68
scripts/setup-test-env Executable file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'request_store'
require 'rake'
require 'active_support/dependencies'
require 'active_support/dependencies/autoload'
require 'active_support/core_ext/numeric'
require 'active_support/string_inquirer'
ENV['SKIP_RAILS_ENV_IN_RAKE'] = 'true'
module Rails
extend self
def root
Pathname.new(File.expand_path('..', __dir__))
end
def env
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
end
ActiveSupport::Dependencies.autoload_paths << 'lib'
load File.expand_path('../lib/tasks/gitlab/helpers.rake', __dir__)
load File.expand_path('../lib/tasks/gitlab/gitaly.rake', __dir__)
# Required for config/0_inject_enterprise_edition_module.rb, lib/gitlab/access.rb
require_dependency File.expand_path('../lib/gitlab', __dir__)
require_dependency File.expand_path('../config/initializers/0_inject_enterprise_edition_module', __dir__)
# Require for lib/gitlab/gitaly_client/storage_settings.rb and config/initializers/1_settings.rb
require 'active_support/hash_with_indifferent_access'
# Required for lib/gitlab/visibility_level.rb and lib/gitlab/safe_request_store.rb
require 'active_support/concern'
require 'active_support/core_ext/module/delegation'
# Required for lib/system_check/helpers.rb
require_dependency File.expand_path('../lib/gitlab/task_helpers', __dir__)
# Required for lib/tasks/gitlab/helpers.rake
require_dependency File.expand_path('../lib/system_check/helpers', __dir__)
# Required for config/initializers/1_settings.rb
require 'omniauth'
require 'omniauth-github'
require 'etc'
require_dependency File.expand_path('../lib/gitlab/access', __dir__)
require_dependency File.expand_path('../config/initializers/1_settings', __dir__)
Gitlab.ee do
load File.expand_path('../ee/lib/tasks/gitlab/indexer.rake', __dir__)
require_dependency File.expand_path('../ee/lib/gitlab/elastic/indexer', __dir__)
require_dependency File.expand_path('../lib/gitlab/utils/override', __dir__)
end
require_dependency File.expand_path('../spec/support/helpers/test_env', __dir__)
TestEnv.init

View File

@ -24,7 +24,10 @@ class StaticAnalysis
(Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 410,
# Most of the time, RuboCop finishes in 30 seconds, but sometimes it can take around 1200 seconds so we set a
# duration of 300 to lower the likelihood that it will run in the same job as another long task...
%w[bundle exec rubocop --parallel] => 300,
%w[bundle exec rubocop --parallel --except Gitlab/MarkUsedFeatureFlags] => 300,
# We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used,
# the cache would prevent these files from being created.
%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false] => 600,
%w[yarn run lint:eslint:all] => 264,
%w[yarn run lint:prettier] => 134,
%w[bin/rake gettext:lint] => 81,

View File

@ -28,6 +28,16 @@ flags_paths = [
# For EE additionally process `ee/` feature flags
if File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
flags_paths << 'ee/config/feature_flags/**/*.yml'
# Geo feature flags are constructed dynamically and there's no explicit checks in the codebase so we mark all
# the replicators' derived feature flags as used.
# See https://gitlab.com/gitlab-org/gitlab/-/blob/54e802e8fe76b6f93656d75ef9b566bf57b60f41/ee/lib/gitlab/geo/replicator.rb#L183-185
Dir.glob('ee/app/replicators/geo/*_replicator.rb').each_with_object(Set.new) do |path, memo|
replicator_name = File.basename(path, '.rb')
feature_flag_name = "geo_#{replicator_name.delete_suffix('_replicator')}_replication"
FileUtils.touch(File.join('tmp', 'feature_flags', "#{feature_flag_name}.used"))
end
end
all_flags = {}
@ -41,7 +51,17 @@ flags_paths.each do |flags_path|
feature_flag_name = File.basename(path, '.yml')
# TODO: we need a better way of tracking use of Gitaly FF across Gitaly and GitLab
next if feature_flag_name.start_with?('gitaly_')
if feature_flag_name.start_with?('gitaly_')
puts "Skipping the #{feature_flag_name} feature flag since it starts with 'gitaly_'."
next
end
# Dynamic feature flag names for redirect to latest CI templates
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144/diffs#fa2193ace3f6a02f7ef9995ef9bc519eca92c4ee_57_84
if feature_flag_name.start_with?('redirect_to_latest_template_')
puts "Skipping the #{feature_flag_name} feature flag since it starts with 'redirect_to_latest_template_'."
next
end
all_flags[feature_flag_name] = File.exist?(File.join('tmp', 'feature_flags', feature_flag_name + '.used'))
end

View File

@ -19,12 +19,6 @@ RSpec.describe ApplicationExperiment, :experiment do
allow(subject).to receive(:enabled?).and_return(true)
end
it "naively assumes a 1x1 relationship to feature flags for tests" do
expect(Feature).to receive(:persist_used!).with('namespaced_stub')
described_class.new('namespaced/stub')
end
it "doesn't raise an exception without a defined control" do
# because we have a default behavior defined

View File

@ -22,3 +22,8 @@ ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
ActiveSupport::XmlMini.backend = 'Nokogiri'
RSpec.configure do |config|
config.filter_run focus: true
config.run_all_when_everything_filtered = true
end

View File

@ -150,6 +150,31 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end
end
describe '#in_graphql_types?' do
%w[
app/graphql/types
ee/app/graphql/ee/types
ee/app/graphql/types
].each do |path|
it "returns true for a node in #{path}" do
node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
expect(cop.in_graphql_types?(node)).to eq(true)
end
end
%w[
app/graphql/resolvers
app/foo
].each do |path|
it "returns true for a node in #{path}" do
node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
expect(cop.in_graphql_types?(node)).to eq(false)
end
end
end
describe '#in_api?' do
it 'returns true for a node in the API directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
@ -164,25 +189,67 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end
end
describe '#in_directory?' do
describe '#in_spec?' do
it 'returns true for a node in the spec directory' do
node = build_and_parse_source('10', rails_root_join('spec', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(true)
end
it 'returns true for a node in the ee/spec directory' do
node = build_and_parse_source('10', rails_root_join('ee', 'spec', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(true)
end
it 'returns false for a node outside the spec directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(false)
end
end
describe '#in_app_directory?' do
it 'returns true for a directory in the CE app/ directory' do
node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
expect(cop.in_app_directory?(node, 'models')).to eq(true)
end
it 'returns true for a directory in the EE app/ directory' do
node =
build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
expect(cop.in_app_directory?(node, 'models')).to eq(true)
end
it 'returns false for a directory in the lib/ directory' do
node =
build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(false)
expect(cop.in_app_directory?(node, 'models')).to eq(false)
end
end
describe '#in_lib_directory?' do
it 'returns true for a directory in the CE lib/ directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(true)
end
it 'returns true for a directory in the EE lib/ directory' do
node =
build_and_parse_source('10', rails_root_join('ee', 'lib', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(true)
end
it 'returns false for a directory in the app/ directory' do
node =
build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(false)
end
end

View File

@ -0,0 +1,233 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/mark_used_feature_flags'
RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
let(:defined_feature_flags) do
%w[a_feature_flag foo_hello foo_world baz_experiment_percentage bar_baz]
end
subject(:cop) { described_class.new }
before do
stub_const("#{described_class}::DYNAMIC_FEATURE_FLAGS", [])
allow(cop).to receive(:defined_feature_flags).and_return(defined_feature_flags)
allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return([])
end
def feature_flag_path(feature_flag_name)
File.expand_path("../../../../tmp/feature_flags/#{feature_flag_name}.used", __dir__)
end
shared_examples 'sets flag as used' do |method_call, flags_to_be_set|
it 'sets the flag as used' do
Array(flags_to_be_set).each do |flag_to_be_set|
expect(FileUtils).to receive(:touch).with(feature_flag_path(flag_to_be_set))
end
expect_no_offenses(<<~RUBY)
class Foo < ApplicationRecord
#{method_call}
end
RUBY
end
end
shared_examples 'does not set any flags as used' do |method_call|
it 'sets the flag as used' do
expect(FileUtils).not_to receive(:touch)
expect_no_offenses(method_call)
end
end
%w[
Feature.enabled?
Feature.disabled?
push_frontend_feature_flag
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'foo'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'foo'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'a string with a "/" in it' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("bar/baz")|, 'bar_baz'
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
Feature::Gitaly.enabled?
Feature::Gitaly.disabled?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'gitaly_foo'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'gitaly_foo'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
experiment
experiment_enabled?
push_frontend_experiment
Gitlab::Experimentation.active?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("baz")|, %w[baz baz_experiment_percentage]
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:baz)|, %w[baz baz_experiment_percentage]
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
use_rugged?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "baz")|, 'baz'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :baz)|, 'baz'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, 123)|
end
end
end
describe 'self.limit_feature_flag = :foo' do
include_examples 'sets flag as used', 'self.limit_feature_flag = :foo', 'foo'
end
describe 'FEATURE_FLAG = :foo' do
include_examples 'sets flag as used', 'FEATURE_FLAG = :foo', 'foo'
end
describe 'Worker `data_consistency` method' do
include_examples 'sets flag as used', 'data_consistency :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'data_consistency :delayed'
end
describe 'Worker `deduplicate` method' do
include_examples 'sets flag as used', 'deduplicate :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'deduplicate :delayed'
end
describe 'GraphQL `field` method' do
before do
allow(cop).to receive(:in_graphql_types?).and_return(true)
end
include_examples 'sets flag as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, feature_flag: :foo', 'foo'
include_examples 'sets flag as used', 'field :runners, null: true, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'field :solution'
include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type'
include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, description: "hello world"'
include_examples 'does not set any flags as used', 'field :solution, type: GraphQL::STRING_TYPE, null: true, description: "URL to the vulnerabilitys details page."'
end
describe "tracking of usage data metrics known events happens at the beginning of inspection" do
let(:usage_data_counters_known_event_feature_flags) { ['an_event_feature_flag'] }
before do
allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return(usage_data_counters_known_event_feature_flags)
end
include_examples 'sets flag as used', "FEATURE_FLAG = :foo", %w[foo an_event_feature_flag]
end
end

View File

@ -230,6 +230,10 @@ RSpec.configure do |config|
Gitlab::Database.set_open_transactions_baseline
end
config.append_before do
Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group]
end
config.append_after do
Gitlab::Database.reset_open_transactions_baseline
end

View File

@ -4,16 +4,6 @@
require 'gitlab/experiment/rspec'
require_relative 'stub_snowplow'
# This is a temporary fix until we have a larger discussion around the
# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104
require Rails.root.join('app', 'experiments', 'application_experiment')
class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def initialize(...)
super(...)
Feature.persist_used!(feature_flag_name)
end
end
RSpec.configure do |config|
config.include StubSnowplow, :experiment

View File

@ -11,7 +11,6 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:active?).and_call_original
experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}")
allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
end
end
@ -26,7 +25,6 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}")
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled }
end
end

View File

@ -4,14 +4,6 @@
module StubbedFeature
extend ActiveSupport::Concern
prepended do
cattr_reader(:persist_used) do
# persist feature flags in CI
# nil: indicates that we do not want to persist used feature flags
Gitlab::Utils.to_boolean(ENV['CI']) ? {} : nil
end
end
class_methods do
# Turn stubbed feature flags on or off.
def stub=(stub)
@ -41,8 +33,6 @@ module StubbedFeature
feature_flag = super
return feature_flag unless stub?
persist_used!(args.first)
# If feature flag is not persisted we mark the feature flag as enabled
# We do `m.call` as we want to validate the execution of method arguments
# and a feature flag state if it is not persisted
@ -52,17 +42,5 @@ module StubbedFeature
feature_flag
end
# This method creates a temporary file in `tmp/feature_flags`
# if feature flag was touched during execution
def persist_used!(name)
return unless persist_used
return if persist_used[name]
persist_used[name] = true
FileUtils.touch(
Rails.root.join('tmp', 'feature_flags', name.to_s + ".used")
)
end
end
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'parallel'
module TestEnv
extend ActiveSupport::Concern
extend self
ComponentFailedToInstallError = Class.new(StandardError)
@ -94,50 +95,40 @@ module TestEnv
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
REPOS_STORAGE = 'default'
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
SETUP_METHODS = %i[setup_gitaly setup_gitlab_shell setup_workhorse setup_factory_repo setup_forked_repo].freeze
# Can be overriden
def setup_methods
SETUP_METHODS
end
# Test environment
#
# See gitlab.yml.example test section for paths
#
def init(opts = {})
def init
unless Rails.env.test?
puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
exit 1
end
start = Time.now
# Disable mailer for spinach tests
disable_mailer if opts[:mailer] == false
clean_test_path
setup_gitlab_shell
setup_gitaly
# Feature specs are run through Workhorse
setup_workhorse
# Create repository for FactoryBot.create(:project)
setup_factory_repo
# Create repository for FactoryBot.create(:forked_project_with_submodules)
setup_forked_repo
end
included do |config|
config.append_before do
set_current_example_group
# Install components in parallel as most of the setup is I/O.
Parallel.each(setup_methods) do |method|
public_send(method)
end
post_init
puts "\nTest environment set up in #{Time.now - start} seconds"
end
def disable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer)
.and_return(double.as_null_object)
end
def enable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer)
.and_call_original
# Can be overriden
def post_init
start_gitaly(gitaly_dir)
end
# Clean /tmp/tests
@ -164,12 +155,11 @@ module TestEnv
end
def setup_gitaly
install_gitaly_args = [gitaly_dir, repos_path, gitaly_url].compact.join(',')
component_timed_setup('Gitaly',
install_dir: gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
task: "gitlab:gitaly:install",
task_args: [gitaly_dir, repos_path, gitaly_url].compact) do
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
@ -190,8 +180,6 @@ module TestEnv
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
start_gitaly(gitaly_dir)
end
def gitaly_socket_path
@ -273,19 +261,18 @@ module TestEnv
raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
end
# Feature specs are run through Workhorse
def setup_workhorse
start = Time.now
return if skip_compile_workhorse?
puts "\n==> Setting up GitLab Workhorse..."
FileUtils.rm_rf(workhorse_dir)
Gitlab::SetupHelper::Workhorse.compile_into(workhorse_dir)
Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil)
File.write(workhorse_tree_file, workhorse_tree) if workhorse_source_clean?
puts " GitLab Workhorse set up in #{Time.now - start} seconds...\n"
puts "==> GitLab Workhorse set up in #{Time.now - start} seconds...\n"
end
def skip_compile_workhorse?
@ -349,10 +336,12 @@ module TestEnv
ENV.fetch('GITLAB_WORKHORSE_URL', nil)
end
# Create repository for FactoryBot.create(:project)
def setup_factory_repo
setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA)
end
# Create repository for FactoryBot.create(:forked_project_with_submodules)
# This repo has a submodule commit that is not present in the main test
# repository.
def setup_forked_repo
@ -363,20 +352,18 @@ module TestEnv
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
puts "\n==> Setting up #{repo_name} repository in #{repo_path}..."
start = Time.now
system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
puts " #{repo_path} set up in #{Time.now - start} seconds...\n"
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
end
set_repo_refs(repo_path, refs)
unless File.directory?(repo_path_bare)
puts "\n==> Setting up #{repo_name} bare repository in #{repo_path_bare}..."
start = Time.now
# We must copy bare repositories because we will push to them.
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare}))
puts " #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
puts "==> #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
end
end
@ -468,10 +455,6 @@ module TestEnv
private
def set_current_example_group
Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group]
end
# These are directories that should be preserved at cleanup time
def test_dirs
@test_dirs ||= %w[
@ -526,7 +509,7 @@ module TestEnv
end
end
def component_timed_setup(component, install_dir:, version:, task:)
def component_timed_setup(component, install_dir:, version:, task:, task_args: [])
start = Time.now
ensure_component_dir_name_is_correct!(component, install_dir)
@ -535,17 +518,22 @@ module TestEnv
return if File.exist?(install_dir) && ci?
if component_needs_update?(install_dir, version)
puts "\n==> Setting up #{component}..."
# Cleanup the component entirely to ensure we start fresh
FileUtils.rm_rf(install_dir)
unless system('rake', task)
raise ComponentFailedToInstallError
if ENV['SKIP_RAILS_ENV_IN_RAKE']
# When we run `scripts/setup-test-env`, we take care of loading the necessary dependencies
# so we can run the rake task programmatically.
Rake::Task[task].invoke(*task_args)
else
# In other cases, we run the task via `rake` so that the environment
# and dependencies are automatically loaded.
raise ComponentFailedToInstallError unless system('rake', "#{task}[#{task_args.join(',')}]")
end
yield if block_given?
puts " #{component} set up in #{Time.now - start} seconds...\n"
puts "==> #{component} set up in #{Time.now - start} seconds...\n"
end
rescue ComponentFailedToInstallError
puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/_global_alert.html.haml' do
before do
allow(view).to receive(:sprite_icon).and_return('<span class="icon"></span>'.html_safe)
end
it 'renders the title' do
title = "The alert's title"
render partial: 'shared/global_alert', locals: { title: title }
expect(rendered).to have_text(title)
end
context 'variants' do
it 'renders an info alert by default' do
render
expect(rendered).to have_selector(".gl-alert-info")
end
%w[warning success danger tip].each do |variant|
it "renders a #{variant} variant" do
allow(view).to receive(:variant).and_return(variant)
render partial: 'shared/global_alert', locals: { variant: variant }
expect(rendered).to have_selector(".gl-alert-#{variant}")
end
end
end
context 'dismissible option' do
it 'shows the dismiss button by default' do
render
expect(rendered).to have_selector('.gl-dismiss-btn')
end
it 'does not show the dismiss button when dismissible is false' do
render partial: 'shared/global_alert', locals: { dismissible: false }
expect(rendered).not_to have_selector('.gl-dismiss-btn')
end
end
context 'fixed layout' do
before do
allow(view).to receive(:fluid_layout).and_return(false)
end
it 'does not add layout limited class' do
render
expect(rendered).not_to have_selector('.gl-alert-layout-limited')
end
it 'adds container classes' do
render
expect(rendered).to have_selector('.container-fluid.container-limited')
end
it 'does not add container classes if is_contained is true' do
render partial: 'shared/global_alert', locals: { is_contained: true }
expect(rendered).not_to have_selector('.container-fluid.container-limited')
end
end
context 'fluid layout' do
before do
allow(view).to receive(:fluid_layout).and_return(true)
render
end
it 'adds layout limited class' do
expect(rendered).to have_selector('.gl-alert-layout-limited')
end
it 'does not add container classes' do
expect(rendered).not_to have_selector('.container-fluid.container-limited')
end
end
end

View File

@ -908,10 +908,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@29.35.0":
version "29.35.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.35.0.tgz#bb04d1e4f8796134bc406adaa869c1b5b1fdcaf2"
integrity sha512-Fso++QXqxZSfIgSmPGlfQC3mdFI6oh/AOL/9cGn4t/3kfxwHd1GCMjUNAFeHsgyIwKIr1hwksipapwuuOIFSCw==
"@gitlab/ui@29.36.0":
version "29.36.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.36.0.tgz#a418c34c7ef768552b551807fa2a65deeaeba0bf"
integrity sha512-ZsaYpbp5cFN9hxVCf19E7avS9AmMaAyS4/Zwkwu2reHJUOkwyOY24eLr44u/Kbaq6SkFarQ2y+zU8vuhzXwQjQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"