Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-11 21:09:09 +00:00
parent 9d195600e6
commit 19d63dbca4
79 changed files with 1482 additions and 1312 deletions

View File

@ -9,10 +9,10 @@ The changes need to become an official part of the product.
- [ ] Determine whether the feature should apply to SaaS and/or self-managed
- [ ] Determine whether the feature should apply to EE - and which tiers - and/or Core
- [ ] Determine if tracking should be kept as is, removed, or modified
- [ ] Determine if tracking should be kept as is, removed, or modified.
- [ ] Ensure any relevant documentation has been updated.
- [ ] Consider changes to any `feature_category:` introduced by the experiment if ownership is changing (PM for Growth and PM for the new category as DRIs)
- [ ] Optional: Migrate experiment to a default enabled [feature flag](https://docs.gitlab.com/ee/development/feature_flags/development.html) for one milestone and add a changelog. Converting to a feature flag can be skipped at the ICs discretion if risk is deemed low with consideration to both SaaS and (if applicable) self managed
- [ ] Optional: Migrate experiment to a default enabled [feature flag](https://docs.gitlab.com/ee/development/feature_flags) for one milestone and add a changelog. Converting to a feature flag can be skipped at the ICs discretion if risk is deemed low with consideration to both SaaS and (if applicable) self managed
- [ ] In the next milestone, [remove the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) if applicable
- [ ] After the flag removal is deployed, [clean up the feature/experiment feature flags](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel

View File

@ -485,7 +485,7 @@ gem 'flipper', '~> 0.17.1'
gem 'flipper-active_record', '~> 0.17.1'
gem 'flipper-active_support_cache_store', '~> 0.17.1'
gem 'unleash', '~> 0.1.5'
gem 'gitlab-experiment', '~> 0.4.12'
gem 'gitlab-experiment', '~> 0.5.0'
# Structured logging
gem 'lograge', '~> 0.5'

View File

@ -430,7 +430,7 @@ GEM
github-markup (1.7.0)
gitlab-chronic (0.10.5)
numerizer (~> 0.2)
gitlab-experiment (0.4.12)
gitlab-experiment (0.5.0)
activesupport (>= 3.0)
scientist (~> 1.5, >= 1.5.0)
gitlab-fog-azure-rm (1.0.1)
@ -1140,7 +1140,7 @@ GEM
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
scientist (1.5.0)
scientist (1.6.0)
securecompare (1.0.0)
seed-fu (2.3.7)
activerecord (>= 3.1)
@ -1413,7 +1413,7 @@ DEPENDENCIES
gitaly (~> 13.9.0.pre.rc1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-experiment (~> 0.4.12)
gitlab-experiment (~> 0.5.0)
gitlab-fog-azure-rm (~> 1.0.1)
gitlab-fog-google (~> 1.13)
gitlab-labkit (~> 0.16.0)

View File

@ -77,9 +77,9 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature flags
Overview and details of feature flag processes in development of GitLab itself is described in [feature flags process documentation](https://docs.gitlab.com/ee/development/feature_flags/process.html).
Overview and details of feature flag processes in development of GitLab itself is described in [feature flags process documentation](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/).
Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/development.html).
Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags).
Getting access and how to expose the feature to users is detailed in [controlling feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/controls.html).

View File

@ -1,3 +1,4 @@
import { isEmpty } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
import { scrollToElement } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@ -88,18 +89,23 @@ export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGet
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, callback },
) =>
service
.update(getters.getNotesData.draftsPath, {
draftId: note.id,
note: noteText,
resolveDiscussion,
position: JSON.stringify(position),
})
) => {
const params = {
draftId: note.id,
note: noteText,
resolveDiscussion,
};
// Stringifying an empty object yields `{}` which breaks graphql queries
// https://gitlab.com/gitlab-org/gitlab/-/issues/298827
if (!isEmpty(position)) params.position = JSON.stringify(position);
return service
.update(getters.getNotesData.draftsPath, params)
.then((res) => res.data)
.then((data) => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data))
.then(callback)
.catch(() => flash(__('An error occurred while updating the comment')));
};
export const scrollToDraft = ({ dispatch, rootGetters }, draft) => {
const discussion = draft.discussion_id && rootGetters.getDiscussion(draft.discussion_id);

View File

@ -129,7 +129,7 @@ export default {
:href="newEnvironmentPath"
data-testid="new-environment"
category="primary"
variant="success"
variant="confirm"
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>
@ -164,7 +164,7 @@ export default {
:href="newEnvironmentPath"
data-testid="new-environment"
category="primary"
variant="success"
variant="confirm"
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>

View File

@ -66,7 +66,6 @@ export default {
<gl-dropdown
v-if="showImportButton"
v-gl-tooltip.hover="__('Import issues')"
data-qa-selector="import_issues_dropdown"
data-testid="import-csv-dropdown"
icon="import"
>

View File

@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import { escape } from 'lodash';
import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import httpStatusCodes from '~/lib/utils/http_status';
@ -282,9 +282,13 @@ export default {
note: {
target_type: this.getNoteableData.targetType,
target_id: this.note.noteable_id,
note: { note: noteText, position: JSON.stringify(position) },
note: { note: noteText },
},
};
// Stringifying an empty object yields `{}` which breaks graphql queries
// https://gitlab.com/gitlab-org/gitlab/-/issues/298827
if (!isEmpty(position)) data.note.note.position = JSON.stringify(position);
this.isRequesting = true;
this.oldContent = this.note.note_html;
// eslint-disable-next-line vue/no-mutating-props

View File

@ -1,3 +1,3 @@
import Todos from './todos';
document.addEventListener('DOMContentLoaded', () => new Todos());
new Todos(); // eslint-disable-line no-new

View File

@ -9,11 +9,15 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
Feature.get(feature_flag_name).state != :off # rubocop:disable Gitlab/AvoidFeatureGet
end
def publish(_result)
def publish(_result = nil)
track(:assignment) # track that we've assigned a variant for this context
# push the experiment data to the client
Gon.push({ experiment: { name => signature } }, true) if in_request_cycle?
begin
Gon.push({ experiment: { name => signature } }, true) # push the experiment data to the client
rescue NoMethodError
# means we're not in the request cycle, and can't add to Gon. Log a warning maybe?
end
end
def track(action, **event_args)
@ -49,12 +53,6 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
name.tr('/', '_')
end
def in_request_cycle?
# Gon is only accessible when having a request. This will be fixed with
# https://gitlab.com/gitlab-org/gitlab/-/issues/323352
context.instance_variable_defined?(:@request)
end
def resolve_variant_name
case rollout_strategy
when :round_robin

View File

@ -22,7 +22,7 @@ module Mutations
response = ::Boards::CreateService.new(board_parent, current_user, args).execute
{
board: response.payload,
board: response.success? ? response.payload : nil,
errors: response.errors
}
end

View File

@ -30,6 +30,10 @@ class Packages::PackageFile < ApplicationRecord
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :for_rubygem_with_file_name, ->(project, file_name) do
joins(:package).merge(project.packages.rubygems).with_file_name(file_name)
end
scope :with_conan_file_type, ->(file_type) do
joins(:conan_file_metadatum)
.where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] })

View File

@ -159,7 +159,7 @@ class Wiki
find_page(SIDEBAR, version)
end
def find_file(name, version = nil)
def find_file(name, version = 'HEAD')
wiki.file(name, version)
end

View File

@ -24,7 +24,7 @@
.text-muted
= html_escape(s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank'
= field.submit _('Save changes'), class: 'btn gl-button btn-success'
= field.submit _('Save changes'), class: 'btn gl-button btn-confirm'
.sub-section.form-group
%h4

View File

@ -4,9 +4,9 @@
.top-area.adjust
.gl-display-block.gl-text-right.gl-my-4.gl-w-full
- if clusterable.can_add_cluster?
= link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-success js-add-cluster gl-py-2', qa_selector: :integrate_kubernetes_cluster_button
= link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-confirm js-add-cluster gl-py-2', qa_selector: :integrate_kubernetes_cluster_button
- else
%span.btn.gl-button.btn-success.js-add-cluster.disabled.gl-py-2
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate")
#js-clusters-list-app{ data: js_clusters_list_data(clusterable.index_path(format: :json)) }

View File

@ -11,4 +11,4 @@
- if clusterable.can_add_cluster?
.gl-text-center
= link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-success', data: { qa_selector: 'add_kubernetes_cluster_link' }
= link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'add_kubernetes_cluster_link' }

View File

@ -55,4 +55,4 @@
= render('clusters/clusters/namespace', platform_field: platform_field, field: field)
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'gl-button btn btn-success'
= field.submit s_('ClusterIntegration|Save changes'), class: 'gl-button btn btn-confirm'

View File

@ -85,4 +85,4 @@
.form-group.js-gke-cluster-creation-submit-container
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
class: 'js-gke-cluster-creation-submit gl-button btn btn-success', disabled: true
class: 'js-gke-cluster-creation-submit gl-button btn btn-confirm', disabled: true

View File

@ -60,4 +60,4 @@
= render('clusters/clusters/namespace', platform_field: platform_kubernetes_field)
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'gl-button btn btn-success', data: { qa_selector: 'add_kubernetes_cluster_button' }
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'add_kubernetes_cluster_button' }

View File

@ -0,0 +1,5 @@
---
title: fix stringify empty position object
merge_request: 56037
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fixed error handling GraphQL API when issue board creation fails
merge_request: 55982
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in clusters directory
merge_request: 56205
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move to confirm variant from success in environments directory
merge_request: 56203
author: Yogi (@yo)
type: changed

View File

@ -13,7 +13,7 @@ module Rack
def log_multipart_warning(req)
content_length = req.content_length.to_i
return unless content_length > 500_000_000
return unless content_length > log_threshold
message = {
message: "Large multipart body detected",
@ -32,6 +32,10 @@ module Rack
def log_large_multipart?
Gitlab::Utils.to_boolean(ENV['ENABLE_RACK_MULTIPART_LOGGING'], default: true) && Gitlab.com?
end
def log_threshold
ENV.fetch('RACK_MULTIPART_LOGGING_BYTES', 100_000_000).to_i
end
end
prepend MultipartPatch

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# rubocop:disable Style/SignalException
SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags/development.html#feature-flag-definition-and-validation)."
SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags#feature-flag-definition-and-validation)."
SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT
```suggestion

View File

@ -8,6 +8,10 @@ class RenameAssetProxyAllowlistOnApplicationSettings < ActiveRecord::Migration[6
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings,
:asset_proxy_whitelist,
:asset_proxy_allowlist
rename_column_concurrently :application_settings,
:asset_proxy_allowlist,
:asset_proxy_whitelist
@ -17,5 +21,9 @@ class RenameAssetProxyAllowlistOnApplicationSettings < ActiveRecord::Migration[6
undo_rename_column_concurrently :application_settings,
:asset_proxy_allowlist,
:asset_proxy_whitelist
undo_cleanup_concurrent_column_rename :application_settings,
:asset_proxy_whitelist,
:asset_proxy_allowlist
end
end

View File

@ -8,14 +8,12 @@ class CleanUpAssetProxyWhitelistRenameOnApplicationSettings < ActiveRecord::Migr
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :application_settings,
:asset_proxy_whitelist,
:asset_proxy_allowlist
# This migration has been made a no-op in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56352
# because to revert the rename in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55419 we need
# to cleanup the triggers on the `asset_proxy_allowlist` column. As such, this migration would do nothing.
end
def down
undo_cleanup_concurrent_column_rename :application_settings,
:asset_proxy_whitelist,
:asset_proxy_allowlist
# no-op
end
end

View File

@ -476,8 +476,8 @@ be mandatory and clients cannot be authenticated with the TLS protocol.
## Multiple LDAP servers **(PREMIUM SELF)**
With GitLab Enterprise Edition Starter, you can configure multiple LDAP servers
that your GitLab instance connects to.
With GitLab, you can configure multiple LDAP servers that your GitLab instance
connects to.
To add another LDAP server:

View File

@ -13,7 +13,7 @@ to deploy features in an early stage of development so that they can be
incrementally rolled out.
Before making them permanently available, features can be deployed behind
flags for a [number of reasons](../development/feature_flags/index.md#when-to-use-feature-flags), such as:
flags for a [number of reasons](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle#when-to-use-feature-flags), such as:
- To test the feature.
- To get feedback from users and customers while in an early stage of the development of the feature.

View File

@ -178,8 +178,8 @@ successfully, you must replicate their data using some other means.
| [Group wiki repository](../../../user/group/index.md#group-wikis) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No | |
| [Uploads](../../uploads.md) | **Yes** (10.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | No | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them. |
| [LFS objects](../../lfs/index.md) | **Yes** (10.2) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8922) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them. GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696). |
| [Personal snippets](../../../user/snippets.md#personal-snippets) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [Project snippets](../../../user/snippets.md#project-snippets) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [CI job artifacts (other than Job Logs)](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta) . | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them |
| [Job logs](../../job_logs.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them |
| [Object pools for forked project deduplication](../../../development/git_object_deduplication.md) | **Yes** | No | No | |

View File

@ -470,7 +470,7 @@ fails. Consider this when toggling the visibility of the feature on or off on
production.
The `feature_flag` property does not allow the use of
[feature gates based on actors](../development/feature_flags/development.md).
[feature gates based on actors](../development/feature_flags/index.md).
This means that the feature flag cannot be toggled only for particular
projects, groups, or users, but instead can only be toggled globally for
everyone.

View File

@ -57,7 +57,7 @@ the `author` field. GitLab team members **should not**.
- Any change behind an enabled feature flag **should** have a changelog entry.
- Any change that adds new usage data metrics and changes that needs to be documented in Product Intelligence [Event Dictionary](https://about.gitlab.com/handbook/product/product-intelligence-guide/#event-dictionary) **should** have a changelog entry.
- A change that adds snowplow events **should** have a changelog entry -
- A change that [removes a feature flag](feature_flags/development.md) **must** have a changelog entry.
- A change that [removes a feature flag](feature_flags/index.md) **must** have a changelog entry.
- A fix for a regression introduced and then fixed in the same release (i.e.,
fixing a bug introduced during a monthly release candidate) **should not**
have a changelog entry.

View File

@ -61,4 +61,4 @@ To request access to ChatOps on GitLab.com:
- [ChatOps Usage](../ci/chatops/index.md)
- [Understanding EXPLAIN plans](understanding_explain_plans.md)
- [Feature Groups](feature_flags/development.md#feature-groups)
- [Feature Groups](feature_flags/index.md#feature-groups)

View File

@ -20,7 +20,7 @@ must be documented. For context, see the
## Criteria
According to the process of [deploying GitLab features behind feature flags](../feature_flags/process.md):
According to the process of [deploying GitLab features behind feature flags](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle):
> - _By default, feature flags should be off._
> - _Feature flags should remain in the codebase for a short period as possible to reduce the need for feature flag accounting._

View File

@ -1611,11 +1611,12 @@ If all content in a section is related, add version text after the header for
the section. The version information must:
- Be surrounded by blank lines.
- Start with `>`.
- Version histories with more than one entry should have each entry on its own
line (long lines are okay). Start each line with `> -` to get unordered list
formatting.
- Whenever possible, have a link to the completed issue, merge request, or epic
- Start with `>`. If there are multiple bullets, each line must start with `> -`.
- The string must include these words in this order (capitalization doesn't matter):
- `introduced`, `deprecated`, `moved`
- `in` or `to`
- `GitLab`
- Whenever possible, include a link to the completed issue, merge request, or epic
that introduced the feature. An issue is preferred over a merge request, and
a merge request is preferred over an epic.

View File

@ -45,7 +45,7 @@ One is built into GitLab directly and has been around for a while (this is calle
[`gitlab-experiment`](https://gitlab.com/gitlab-org/gitlab-experiment) and is referred
to as `Gitlab::Experiment` -- GLEX for short.
Both approaches use [experiment](../feature_flags/development.md#experiment-type)
Both approaches use [experiment](../feature_flags/index.md#experiment-type)
feature flags, and there is currently no strong suggestion to use one over the other.
| Feature | `Experimentation Module` | GLEX |

View File

@ -423,7 +423,7 @@ query getAuthorData($authorNameEnabled: Boolean = false) {
```
Then in the Vue (or JavaScript) call to the query we can pass in our feature flag. This feature
flag needs to be already set up correctly. See the [feature flag documentation](../feature_flags/development.md)
flag needs to be already set up correctly. See the [feature flag documentation](../feature_flags/index.md)
for the correct way to do this.
```javascript

View File

@ -36,7 +36,7 @@ easier to measure the impact of both separately.
The GitLab feature library (using
[Flipper](https://github.com/jnunemaker/flipper), and covered in the [Feature
Flags process](process.md) guide) supports rolling out changes to a percentage of
Flags process](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle) guide) supports rolling out changes to a percentage of
time to users. This in turn can be controlled using [GitLab ChatOps](../../ci/chatops/index.md).
For an up to date list of feature flag commands please see [the source
@ -240,7 +240,7 @@ To disable a feature flag that has been enabled for a specific project you can r
/chatops run feature set --group=gitlab-org some_feature false
```
You cannot selectively disable feature flags for a specific project/group/user without applying a [specific method of implementing](development.md#selectively-disable-by-actor) the feature flags.
You cannot selectively disable feature flags for a specific project/group/user without applying a [specific method of implementing](index.md#selectively-disable-by-actor) the feature flags.
### Feature flag change logging
@ -281,7 +281,7 @@ To remove a feature flag, open **one merge request** to make the changes. In the
1. Add the ~"feature flag" label so release managers are aware the changes are hidden behind a feature flag.
1. If the merge request has to be picked into a stable branch, add the
appropriate `~"Pick into X.Y"` label, for example `~"Pick into 13.0"`.
See [the feature flag process](process.md#including-a-feature-behind-feature-flag-in-the-final-release)
See [the feature flag process](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle#including-a-feature-behind-feature-flag-in-the-final-release)
for further details.
1. Remove all references to the feature flag from the codebase, including tests.
1. Remove the YAML definition for the feature from the repository.

View File

@ -1,604 +1,7 @@
---
type: reference, dev
stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
redirect_to: 'index.md'
---
# Developing with feature flags
This document provides guidelines on how to use feature flags
in the GitLab codebase to conditionally enable features
and test them.
Features that are developed and merged behind a feature flag
should not include a changelog entry. The entry should be added either in the merge
request removing the feature flag or the merge request where the default value of
the feature flag is set to enabled. If the feature contains any database migrations, it
*should* include a changelog entry for the database changes.
WARNING:
All newly-introduced feature flags should be [disabled by default](process.md#feature-flags-in-gitlab-development).
NOTE:
This document is the subject of continued work as part of an epic to [improve internal usage of Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). Raise any suggestions as new issues and attach them to the epic.
## Risk of a broken master (main) branch
Feature flags **must** be used in the MR that introduces them. Not doing so causes a
[broken master](https://about.gitlab.com/handbook/engineering/workflow/#broken-master) scenario due
to the `rspec:feature-flags` job that only runs on the `master` branch.
## Types of feature flags
Choose a feature flag type that matches the expected usage.
### `development` type
`development` feature flags are short-lived feature flags,
used so that unfinished code can be deployed in production.
A `development` feature flag should have a rollout issue,
ideally created using the [Feature Flag Roll Out template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md).
This is the default type used when calling `Feature.enabled?`.
### `ops` type
`ops` feature flags are long-lived feature flags that control operational aspects
of GitLab product behavior. For example, feature flags that disable features that might
have a performance impact, like special Sidekiq worker behavior.
`ops` feature flags likely do not have rollout issues, as it is hard to
predict when they are enabled or disabled.
To use `ops` feature flags, you must append `type: :ops` to `Feature.enabled?`
invocations:
```ruby
# Check if feature flag is enabled
Feature.enabled?(:my_ops_flag, project, type: :ops)
# Check if feature flag is disabled
Feature.disabled?(:my_ops_flag, project, type: :ops)
# Push feature flag to Frontend
push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
```
### `experiment` type
`experiment` feature flags are used for A/B testing on GitLab.com.
An `experiment` feature flag should conform to the same standards as a `development` feature flag,
although the interface has some differences. An experiment feature flag should have a rollout issue,
ideally created using the [Experiment Tracking template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/experiment_tracking_template.md). More information can be found in the [experiment guide](../experiment_guide/index.md).
## Feature flag definition and validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229161) in GitLab 13.3.
During development (`RAILS_ENV=development`) or testing (`RAILS_ENV=test`) all feature flag usage is being strictly validated.
This process is meant to ensure consistent feature flag usage in the codebase. All feature flags **must**:
- Be known. Only use feature flags that are explicitly defined.
- Not be defined twice. They have to be defined either in FOSS or EE, but not both.
- Use a valid and consistent `type:` across all invocations.
- Use the same `default_enabled:` across all invocations.
- Have an owner.
All feature flags known to GitLab are self-documented in YAML files stored in:
- [`config/feature_flags`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/feature_flags)
- [`ee/config/feature_flags`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/feature_flags)
Each feature flag is defined in a separate YAML file consisting of a number of fields:
| Field | Required | Description |
|---------------------|----------|----------------------------------------------------------------|
| `name` | yes | Name of the feature flag. |
| `type` | yes | Type of feature flag. |
| `default_enabled` | yes | The default state of the feature flag that is strictly validated, with `default_enabled:` passed as an argument. |
| `introduced_by_url` | no | The URL to the Merge Request that introduced the feature flag. |
| `rollout_issue_url` | no | The URL to the Issue covering the feature flag rollout. |
| `group` | no | The [group](https://about.gitlab.com/handbook/product/categories/#devops-stages) that owns the feature flag. |
NOTE:
All validations are skipped when running in `RAILS_ENV=production`.
## Create a new feature flag
The GitLab codebase provides [`bin/feature-flag`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/bin/feature-flag),
a dedicated tool to create new feature flag definitions.
The tool asks various questions about the new feature flag, then creates
a YAML definition in `config/feature_flags` or `ee/config/feature_flags`.
Only feature flags that have a YAML definition file can be used when running the development or testing environments.
```shell
$ bin/feature-flag my_feature_flag
>> Specify the group introducing the feature flag, like `group::apm`:
?> group::memory
>> URL of the MR introducing the feature flag (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
>> Open this URL and fill in the rest of the details:
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out
>> URL of the rollout issue (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533
create config/feature_flags/development/my_feature_flag.yml
---
name: my_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232533
group: group::memory
type: development
default_enabled: false
```
NOTE:
To create a feature flag that is only used in EE, add the `--ee` flag: `bin/feature-flag --ee`
## Delete a feature flag
See [cleaning up feature flags](controls.md#cleaning-up) for more information about
deleting feature flags.
## Develop with a feature flag
There are two main ways of using Feature Flags in the GitLab codebase:
- [Backend code (Rails)](#backend)
- [Frontend code (VueJS)](#frontend)
### Backend
The feature flag interface is defined in [`lib/feature.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/feature.rb).
This interface provides a set of methods to check if the feature flag is enabled or disabled:
```ruby
if Feature.enabled?(:my_feature_flag, project)
# execute code if feature flag is enabled
else
# execute code if feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project)
# execute code if feature flag is disabled
end
```
In rare cases you may want to make a feature enabled by default. If so, explain the reasoning
in the merge request. Use `default_enabled: true` when checking the feature flag state:
```ruby
if Feature.enabled?(:feature_flag, project, default_enabled: true)
# execute code if feature flag is enabled
else
# execute code if feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project, default_enabled: true)
# execute code if feature flag is disabled
end
```
If not specified, `default_enabled` is `false`.
To force reading the `default_enabled` value from the relative YAML definition file, use
`default_enabled: :yaml`:
```ruby
if Feature.enabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is enabled
end
```
```ruby
if Feature.disabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is disabled
end
```
This allows to use the same feature flag check across various parts of the codebase and
maintain the status of `default_enabled` in the YAML definition file which is the SSOT.
If `default_enabled: :yaml` is used, a YAML definition is expected or an error is raised
in development or test environment, while returning `false` on production.
If not specified, the default feature flag type for `Feature.enabled?` and `Feature.disabled?`
is `type: development`. For all other feature flag types, you must specify the `type:`:
```ruby
if Feature.enabled?(:feature_flag, project, type: :ops)
# execute code if ops feature flag is enabled
else
# execute code if ops feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project, type: :ops)
# execute code if feature flag is disabled
end
```
WARNING:
Don't use feature flags at application load time. For example, using the `Feature` class in
`config/initializers/*` or at the class level could cause an unexpected error. This error occurs
because a database that a feature flag adapter might depend on doesn't exist at load time
(especially for fresh installations). Checking for the database's existence at the caller isn't
recommended, as some adapters don't require a database at all (for example, the HTTP adapter). The
feature flag setup check must be abstracted in the `Feature` namespace. This approach also requires
application reload when the feature flag changes. You must therefore ask SREs to reload the
Web/API/Sidekiq fleet on production, which takes time to fully rollout/rollback the changes. For
these reasons, use environment variables (for example, `ENV['YOUR_FEATURE_NAME']`) or `gitlab.yml`
instead.
Here's an example of a pattern that you should avoid:
```ruby
class MyClass
if Feature.enabled?(:...)
new_process
else
legacy_process
end
end
```
### Frontend
Use the `push_frontend_feature_flag` method for frontend code, which is
available to all controllers that inherit from `ApplicationController`. You can use
this method to expose the state of a feature flag, for example:
```ruby
before_action do
# Prefer to scope it per project or user e.g.
push_frontend_feature_flag(:vim_bindings, project)
end
def index
# ...
end
def edit
# ...
end
```
You can then check the state of the feature flag in JavaScript as follows:
```javascript
if ( gon.features.vimBindings ) {
// ...
}
```
The name of the feature flag in JavaScript is always camelCase,
so checking for `gon.features.vim_bindings` would not work.
See the [Vue guide](../fe_guide/vue.md#accessing-feature-flags) for details about
how to access feature flags in a Vue component.
In rare cases you may want to make a feature enabled by default. If so, explain the reasoning
in the merge request. Use `default_enabled: true` when checking the feature flag state:
```ruby
before_action do
# Prefer to scope it per project or user e.g.
push_frontend_feature_flag(:vim_bindings, project, default_enabled: true)
end
```
If not specified, the default feature flag type for `push_frontend_feature_flag`
is `type: development`. For all other feature flag types, you must specify the `type:`:
```ruby
before_action do
push_frontend_feature_flag(:vim_bindings, project, type: :ops)
end
```
### Feature actors
**It is strongly advised to use actors with feature flags.** Actors provide a simple
way to enable a feature flag only for a given project, group or user. This makes debugging
easier, as you can filter logs and errors for example, based on actors. This also makes it possible
to enable the feature on the `gitlab-org` or `gitlab-com` groups first, while the rest of
the users aren't impacted.
Actors also provide an easy way to do a percentage rollout of a feature in a sticky way.
If a 1% rollout enabled a feature for a specific actor, that actor will continue to have the feature enabled at
10%, 50%, and 100%.
GitLab currently supports the following models as feature flag actors:
- `User`
- `Project`
- `Group`
The actor is a second parameter of the `Feature.enabled?` call. The
same actor type must be used consistently for all invocations of `Feature.enabled?`.
```ruby
Feature.enabled?(:feature_flag, project)
Feature.enabled?(:feature_flag, group)
Feature.enabled?(:feature_flag, user)
```
#### Selectively disable by actor
By default you cannot selectively disable a feature flag by actor.
```shell
# This will not work how you would expect.
/chatops run feature set some_feature true
/chatops run feature set --project=gitlab-org/gitlab some_feature false
```
However, if you add two feature flags, you can write your conditional statement in such a way that the equivalent selective disable is possible.
```ruby
Feature.enabled?(:a_feature, project) && Feature.disabled?(:a_feature_override, project)
```
```shell
# This will enable a feature flag globally, except for gitlab-org/gitlab
/chatops run feature set a_feature true
/chatops run feature set --project=gitlab-org/gitlab a_feature_override true
```
### Enable additional objects as actors
To use feature gates based on actors, the model needs to respond to
`flipper_id`. For example, to enable for the Foo model:
```ruby
class Foo < ActiveRecord::Base
include FeatureGate
end
```
Only models that `include FeatureGate` or expose `flipper_id` method can be
used as an actor for `Feature.enabled?`.
### Feature flags for licensed features
You can't use a feature flag with the same name as a licensed feature name, because
it would cause a naming collision. This was [widely discussed and removed](https://gitlab.com/gitlab-org/gitlab/-/issues/259611)
because it is confusing.
To check for licensed features, add a dedicated feature flag under a different name
and check it explicitly, for example:
```ruby
Feature.enabled?(:licensed_feature_feature_flag, project) &&
project.feature_available?(:licensed_feature)
```
### Feature groups
Feature groups must be defined statically in `lib/feature.rb` (in the
`.register_feature_groups` method), but their implementation can obviously be
dynamic (querying the DB, for example).
Once defined in `lib/feature.rb`, you can to activate a
feature for a given feature group via the [`feature_group` parameter of the features API](../../api/features.md#set-or-create-a-feature)
### Enabling a feature flag locally (in development)
In the rails console (`rails c`), enter the following command to enable a feature flag:
```ruby
Feature.enable(:feature_flag_name)
```
Similarly, the following command disables a feature flag:
```ruby
Feature.disable(:feature_flag_name)
```
You can also enable a feature flag for a given gate:
```ruby
Feature.enable(:feature_flag_name, Project.find_by_full_path("root/my-project"))
```
### Removing a feature flag locally (in development)
Once you have manually enabled or disabled a feature flag to test in your local environment,
the flag's default value gets overwritten and it takes precedence over the `default_enabled` value.
This can cause confusion when changing the flag's `default_enabled` attribute.
For example, flags are commonly enabled and disabled several times during the development process.
When we finally enable the flag by default, we set `default_enabled: true` in the YAML file.
- If the flag was manually enabled before setting `default_enabled: true`, the feature will be enabled.
Not because of the `default_enabled: true` value of the flag but because it was manually enabled.
- If the flag was manually disabled before setting `default_enabled: true`, the feature will
remain disabled. The `default_enabled: true` value does not take precendence over the explicit `false`
value set when disabling it manually.
To reset the feature flag to the default status set in its YAML file, remove it using the Rails console
(`rails c`) as follows:
```ruby
Feature.remove(:feature_flag_name)
```
## Feature flags in tests
Introducing a feature flag into the codebase creates an additional code path that should be tested.
It is strongly advised to test all code affected by a feature flag, both when **enabled** and **disabled**
to ensure the feature works properly.
When using the testing environment, all feature flags are enabled by default.
WARNING:
This does not apply to end-to-end (QA) tests, which [do not disable feature flags by default](#end-to-end-qa-tests). There is a different [process for using feature flags in end-to-end tests](../testing_guide/end_to_end/feature_flags.md).
To disable a feature flag in a test, use the `stub_feature_flags`
helper. For example, to globally disable the `ci_live_trace` feature
flag in a test:
```ruby
stub_feature_flags(ci_live_trace: false)
Feature.enabled?(:ci_live_trace) # => false
```
If you wish to set up a test where a feature flag is enabled only
for some actors and not others, you can specify this in options
passed to the helper. For example, to enable the `ci_live_trace`
feature flag for a specific project:
```ruby
project1, project2 = build_list(:project, 2)
# Feature will only be enabled for project1
stub_feature_flags(ci_live_trace: project1)
Feature.enabled?(:ci_live_trace) # => false
Feature.enabled?(:ci_live_trace, project1) # => true
Feature.enabled?(:ci_live_trace, project2) # => false
```
The behavior of FlipperGate is as follows:
1. You can enable an override for a specified actor to be enabled.
1. You can disable (remove) an override for a specified actor,
falling back to the default state.
1. There's no way to model that you explicitly disabled a specified actor.
```ruby
Feature.enable(:my_feature)
Feature.disable(:my_feature, project1)
Feature.enabled?(:my_feature) # => true
Feature.enabled?(:my_feature, project1) # => true
Feature.disable(:my_feature2)
Feature.enable(:my_feature2, project1)
Feature.enabled?(:my_feature2) # => false
Feature.enabled?(:my_feature2, project1) # => true
```
### `have_pushed_frontend_feature_flags`
Use `have_pushed_frontend_feature_flags` to test if [`push_frontend_feature_flag`](#frontend)
has added the feature flag to the HTML.
For example,
```ruby
stub_feature_flags(value_stream_analytics_path_navigation: false)
visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
```
### `stub_feature_flags` vs `Feature.enable*`
It is preferred to use `stub_feature_flags` to enable feature flags
in the testing environment. This method provides a simple and well described
interface for simple use cases.
However, in some cases more complex behavior needs to be tested,
like percentage rollouts of feature flags. This can be done using
`.enable_percentage_of_time` or `.enable_percentage_of_actors`:
```ruby
# Good: feature needs to be explicitly disabled, as it is enabled by default if not defined
stub_feature_flags(my_feature: false)
stub_feature_flags(my_feature: true)
stub_feature_flags(my_feature: project)
stub_feature_flags(my_feature: [project, project2])
# Bad
Feature.enable(:my_feature_2)
# Good: enable my_feature for 50% of time
Feature.enable_percentage_of_time(:my_feature_3, 50)
# Good: enable my_feature for 50% of actors/gates/things
Feature.enable_percentage_of_actors(:my_feature_4, 50)
```
Each feature flag that has a defined state is persisted
during test execution time:
```ruby
Feature.persisted_names.include?('my_feature') => true
Feature.persisted_names.include?('my_feature_2') => true
Feature.persisted_names.include?('my_feature_3') => true
Feature.persisted_names.include?('my_feature_4') => true
```
### Stubbing actor
When you want to enable a feature flag for a specific actor only,
you can stub its representation. A gate that is passed
as an argument to `Feature.enabled?` and `Feature.disabled?` must be an object
that includes `FeatureGate`.
In specs you can use the `stub_feature_flag_gate` method that allows you to
quickly create a custom actor:
```ruby
gate = stub_feature_flag_gate('CustomActor')
stub_feature_flags(ci_live_trace: gate)
Feature.enabled?(:ci_live_trace) # => false
Feature.enabled?(:ci_live_trace, gate) # => true
```
You can also disable a feature flag for a specific actor:
```ruby
gate = stub_feature_flag_gate('CustomActor')
stub_feature_flags(ci_live_trace: false, thing: gate)
```
### Controlling feature flags engine in tests
Our Flipper engine in the test environment works in a memory mode `Flipper::Adapters::Memory`.
`production` and `development` modes use `Flipper::Adapters::ActiveRecord`.
You can control whether the `Flipper::Adapters::Memory` or `ActiveRecord` mode is being used.
#### `stub_feature_flags: true` (default and preferred)
In this mode Flipper is configured to use `Flipper::Adapters::Memory` and mark all feature
flags to be on-by-default and persisted on a first use. This overwrites the `default_enabled:`
of `Feature.enabled?` and `Feature.disabled?` returning always `true` unless feature flag
is persisted.
Make sure behavior under feature flag doesn't go untested in some non-specific contexts.
### `stub_feature_flags: false`
This disables a memory-stubbed flipper, and uses `Flipper::Adapters::ActiveRecord`
a mode that is used by `production` and `development`.
You should use this mode only when you really want to tests aspects of Flipper
with how it interacts with `ActiveRecord`.
### End-to-end (QA) tests
Toggling feature flags works differently in end-to-end (QA) tests. The end-to-end test framework does not have direct access to
Rails or the database, so it can't use Flipper. Instead, it uses [the public API](../../api/features.md#set-or-create-a-feature). Each end-to-end test can [enable or disable a feature flag during the test](../testing_guide/end_to_end/feature_flags.md). Alternatively, you can enable or disable a feature flag before one or more tests when you [run them from your GitLab repository's `qa` directory](https://gitlab.com/gitlab-org/gitlab/tree/master/qa#running-tests-with-a-feature-flag-enabled-or-disabled), or if you [run the tests via GitLab QA](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#running-tests-with-a-feature-flag-enabled).
[As noted above, feature flags are not enabled by default in end-to-end tests.](#feature-flags-in-tests)
This means that end-to-end tests will run with feature flags in the default state implemented in the source
code, or with the feature flag in its current state on the GitLab instance under test, unless the
test is written to enable/disable a feature flag explicitly.
When a feature flag is changed on Staging or on GitLab.com, a Slack message will be posted to the `#qa-staging` or `#qa-production` channels to inform
the pipeline triage DRI so that they can more easily determine if any failures are related to a feature flag change. However, if you are working on a change you can
help to avoid unexpected failures by [confirming that the end-to-end tests pass with a feature flag enabled.](../testing_guide/end_to_end/feature_flags.md#confirming-that-end-to-end-tests-pass-with-a-feature-flag-enabled)
This document was moved to [another location](index.md).
<!-- This redirect file can be deleted after 2021-06-01. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -1,75 +1,636 @@
---
type: reference, dev
stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
---
# Feature flags in development of GitLab
# Developing with feature flags
**NOTE**:
The documentation below covers feature flags used by GitLab to deploy its own features, which **is not** the same
as the [feature flags offered as part of the product](../../operations/feature_flags.md).
## When to use feature flags
This document provides guidelines on how to use feature flags
in the GitLab codebase to conditionally enable features
and test them.
Developers are required to use feature flags for changes that could affect availability of existing GitLab functionality (if it only affects the new feature you're making that is probably acceptable).
Such changes include:
Features that are developed and merged behind a feature flag
should not include a changelog entry. The entry should be added either in the merge
request removing the feature flag or the merge request where the default value of
the feature flag is set to enabled. If the feature contains any database migrations, it
*should* include a changelog entry for the database changes.
1. New features in high traffic areas (e.g. a new merge request widget, new option in issues/epics, new CI functionality).
1. Complex performance improvements that may require additional testing in production (e.g. rewriting complex queries, changes to frequently used API endpoints).
1. Invasive changes to the user interface (e.g. introducing a new navigation bar, removal of a sidebar, UI element change in issues or MR interface).
1. Introducing dependencies on third-party services (e.g. adding support for importing projects).
1. Changes to features that can cause data corruption or cause data loss (e.g. features processing repository data or user uploaded content).
WARNING:
All newly-introduced feature flags should be [disabled by default](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#feature-flags-in-gitlab-development).
Situations where you might consider not using a feature flag:
NOTE:
This document is the subject of continued work as part of an epic to [improve internal usage of Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). Raise any suggestions as new issues and attach them to the epic.
1. Adding a new API endpoint
1. Introducing new features in low traffic areas (e.g. adding a new export functionality in the admin area/group settings/project settings)
1. Non-invasive frontend changes (e.g. changing the color of a button, or moving a UI element in a low traffic area)
## Feature flags in GitLab development
In all cases, those working on the changes should ask themselves:
The following highlights should be considered when deciding if feature flags
should be leveraged:
> Why do I need to add a feature flag? If I don't add one, what options do I have to control the impact on application reliability, and user experience?
- By default, the feature flags should be **off**.
- Feature flags should remain in the codebase for as short period as possible
to reduce the need for feature flag accounting.
- The person operating with feature flags is responsible for clearly communicating
the status of a feature behind the feature flag with responsible stakeholders. The
issue description should be updated with the feature flag name and whether it is
defaulted on or off as soon it is evident that a feature flag is needed.
- Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable must have the
~"feature flag" label assigned.
- When development of a feature will be spread across multiple merge
requests, you can use the following workflow:
For perspective on why we limit our use of feature flags please see
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Feature flags only when needed](https://www.youtube.com/watch?v=DQaGqyolOd8).
1. [Create a new feature flag](#create-a-new-feature-flag)
which is **off** by default, in the first merge request which uses the flag.
Flags [should not be added separately](#risk-of-a-broken-master-main-branch).
1. Submit incremental changes via one or more merge requests, ensuring that any
new code added can only be reached if the feature flag is **on**.
You can keep the feature flag enabled on your local GDK during development.
1. When the feature is ready to be tested, enable the feature flag for
a specific project and ensure that there are no issues with the implementation.
1. When the feature is ready to be announced, create a merge request that adds
documentation about the feature, including [documentation for the feature flag itself](../documentation/feature_flags.md),
and a changelog entry. In the same merge request either flip the feature flag to
be **on by default** or remove it entirely in order to enable the new behavior.
In case you are uncertain if a feature flag is necessary, simply ask about this in an early merge request, and those reviewing the changes will likely provide you with an answer.
One might be tempted to think that feature flags will delay the release of a
feature by at least one month (= one release). This is not the case. A feature
flag does not have to stick around for a specific amount of time
(e.g. at least one release), instead they should stick around until the feature
is deemed stable. Stable means it works on GitLab.com without causing any
problems, such as outages.
When using a feature flag for UI elements, make sure to _also_ use a feature
flag for the underlying backend code, if there is any. This ensures there is
absolutely no way to use the feature until it is enabled.
## Risk of a broken master (main) branch
## How to use Feature Flags
Feature flags **must** be used in the MR that introduces them. Not doing so causes a
[broken master](https://about.gitlab.com/handbook/engineering/workflow/#broken-master) scenario due
to the `rspec:feature-flags` job that only runs on the `master` branch.
Feature flags can be used to gradually deploy changes, regardless of whether
they are new features or performance improvements. By using feature flags,
you can determine the impact of GitLab-directed changes, while still being able
to disable those changes without having to revert an entire release.
## Types of feature flags
For an overview about starting with feature flags in GitLab development,
use this [training template](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/.gitlab/issue_templates/feature-flag-training.md).
Choose a feature flag type that matches the expected usage.
Before using feature flags for GitLab development, review the following development guides:
### `development` type
1. [Process for using features flags](process.md): When you should use
feature flags in the development of GitLab, what's the cost of using them,
and how to include them in a release.
1. [Developing with feature flags](development.md): Learn about the types of
feature flags, their definition and validation, how to create them, frontend and
backend details, and other information.
1. [Documenting features deployed behind feature flags](../documentation/feature_flags.md):
How to document features deployed behind feature flags, and how to update the
documentation for features' flags when their states change.
1. [Controlling feature flags](controls.md): Learn the process for deploying
a new feature, enabling it on GitLab.com, communicating the change,
logging, and cleaning up.
`development` feature flags are short-lived feature flags,
used so that unfinished code can be deployed in production.
User guides:
A `development` feature flag should have a rollout issue,
ideally created using the [Feature Flag Roll Out template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md).
1. [How GitLab administrators can enable and disable features behind flags](../../administration/feature_flags.md):
An explanation for GitLab administrators about how they can
enable or disable GitLab features behind feature flags.
1. [What "features deployed behind flags" means to the GitLab user](../../user/feature_flags.md):
An explanation for GitLab users regarding how certain features
might not be available to them until they are enabled.
This is the default type used when calling `Feature.enabled?`.
### `ops` type
`ops` feature flags are long-lived feature flags that control operational aspects
of GitLab product behavior. For example, feature flags that disable features that might
have a performance impact, like special Sidekiq worker behavior.
`ops` feature flags likely do not have rollout issues, as it is hard to
predict when they are enabled or disabled.
To use `ops` feature flags, you must append `type: :ops` to `Feature.enabled?`
invocations:
```ruby
# Check if feature flag is enabled
Feature.enabled?(:my_ops_flag, project, type: :ops)
# Check if feature flag is disabled
Feature.disabled?(:my_ops_flag, project, type: :ops)
# Push feature flag to Frontend
push_frontend_feature_flag(:my_ops_flag, project, type: :ops)
```
### `experiment` type
`experiment` feature flags are used for A/B testing on GitLab.com.
An `experiment` feature flag should conform to the same standards as a `development` feature flag,
although the interface has some differences. An experiment feature flag should have a rollout issue,
ideally created using the [Experiment Tracking template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/experiment_tracking_template.md). More information can be found in the [experiment guide](../experiment_guide/index.md).
## Feature flag definition and validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229161) in GitLab 13.3.
During development (`RAILS_ENV=development`) or testing (`RAILS_ENV=test`) all feature flag usage is being strictly validated.
This process is meant to ensure consistent feature flag usage in the codebase. All feature flags **must**:
- Be known. Only use feature flags that are explicitly defined.
- Not be defined twice. They have to be defined either in FOSS or EE, but not both.
- Use a valid and consistent `type:` across all invocations.
- Use the same `default_enabled:` across all invocations.
- Have an owner.
All feature flags known to GitLab are self-documented in YAML files stored in:
- [`config/feature_flags`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/config/feature_flags)
- [`ee/config/feature_flags`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/config/feature_flags)
Each feature flag is defined in a separate YAML file consisting of a number of fields:
| Field | Required | Description |
|---------------------|----------|----------------------------------------------------------------|
| `name` | yes | Name of the feature flag. |
| `type` | yes | Type of feature flag. |
| `default_enabled` | yes | The default state of the feature flag that is strictly validated, with `default_enabled:` passed as an argument. |
| `introduced_by_url` | no | The URL to the Merge Request that introduced the feature flag. |
| `rollout_issue_url` | no | The URL to the Issue covering the feature flag rollout. |
| `group` | no | The [group](https://about.gitlab.com/handbook/product/categories/#devops-stages) that owns the feature flag. |
NOTE:
All validations are skipped when running in `RAILS_ENV=production`.
## Create a new feature flag
The GitLab codebase provides [`bin/feature-flag`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/bin/feature-flag),
a dedicated tool to create new feature flag definitions.
The tool asks various questions about the new feature flag, then creates
a YAML definition in `config/feature_flags` or `ee/config/feature_flags`.
Only feature flags that have a YAML definition file can be used when running the development or testing environments.
```shell
$ bin/feature-flag my_feature_flag
>> Specify the group introducing the feature flag, like `group::apm`:
?> group::memory
>> URL of the MR introducing the feature flag (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
>> Open this URL and fill in the rest of the details:
https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out
>> URL of the rollout issue (enter to skip):
?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533
create config/feature_flags/development/my_feature_flag.yml
---
name: my_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232533
group: group::memory
type: development
default_enabled: false
```
NOTE:
To create a feature flag that is only used in EE, add the `--ee` flag: `bin/feature-flag --ee`
## Delete a feature flag
See [cleaning up feature flags](controls.md#cleaning-up) for more information about
deleting feature flags.
## Develop with a feature flag
There are two main ways of using Feature Flags in the GitLab codebase:
- [Backend code (Rails)](#backend)
- [Frontend code (VueJS)](#frontend)
### Backend
The feature flag interface is defined in [`lib/feature.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/feature.rb).
This interface provides a set of methods to check if the feature flag is enabled or disabled:
```ruby
if Feature.enabled?(:my_feature_flag, project)
# execute code if feature flag is enabled
else
# execute code if feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project)
# execute code if feature flag is disabled
end
```
In rare cases you may want to make a feature enabled by default. If so, explain the reasoning
in the merge request. Use `default_enabled: true` when checking the feature flag state:
```ruby
if Feature.enabled?(:feature_flag, project, default_enabled: true)
# execute code if feature flag is enabled
else
# execute code if feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project, default_enabled: true)
# execute code if feature flag is disabled
end
```
If not specified, `default_enabled` is `false`.
To force reading the `default_enabled` value from the relative YAML definition file, use
`default_enabled: :yaml`:
```ruby
if Feature.enabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is enabled
end
```
```ruby
if Feature.disabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is disabled
end
```
This allows to use the same feature flag check across various parts of the codebase and
maintain the status of `default_enabled` in the YAML definition file which is the SSOT.
If `default_enabled: :yaml` is used, a YAML definition is expected or an error is raised
in development or test environment, while returning `false` on production.
If not specified, the default feature flag type for `Feature.enabled?` and `Feature.disabled?`
is `type: development`. For all other feature flag types, you must specify the `type:`:
```ruby
if Feature.enabled?(:feature_flag, project, type: :ops)
# execute code if ops feature flag is enabled
else
# execute code if ops feature flag is disabled
end
if Feature.disabled?(:my_feature_flag, project, type: :ops)
# execute code if feature flag is disabled
end
```
WARNING:
Don't use feature flags at application load time. For example, using the `Feature` class in
`config/initializers/*` or at the class level could cause an unexpected error. This error occurs
because a database that a feature flag adapter might depend on doesn't exist at load time
(especially for fresh installations). Checking for the database's existence at the caller isn't
recommended, as some adapters don't require a database at all (for example, the HTTP adapter). The
feature flag setup check must be abstracted in the `Feature` namespace. This approach also requires
application reload when the feature flag changes. You must therefore ask SREs to reload the
Web/API/Sidekiq fleet on production, which takes time to fully rollout/rollback the changes. For
these reasons, use environment variables (for example, `ENV['YOUR_FEATURE_NAME']`) or `gitlab.yml`
instead.
Here's an example of a pattern that you should avoid:
```ruby
class MyClass
if Feature.enabled?(:...)
new_process
else
legacy_process
end
end
```
### Frontend
Use the `push_frontend_feature_flag` method for frontend code, which is
available to all controllers that inherit from `ApplicationController`. You can use
this method to expose the state of a feature flag, for example:
```ruby
before_action do
# Prefer to scope it per project or user e.g.
push_frontend_feature_flag(:vim_bindings, project)
end
def index
# ...
end
def edit
# ...
end
```
You can then check the state of the feature flag in JavaScript as follows:
```javascript
if ( gon.features.vimBindings ) {
// ...
}
```
The name of the feature flag in JavaScript is always camelCase,
so checking for `gon.features.vim_bindings` would not work.
See the [Vue guide](../fe_guide/vue.md#accessing-feature-flags) for details about
how to access feature flags in a Vue component.
In rare cases you may want to make a feature enabled by default. If so, explain the reasoning
in the merge request. Use `default_enabled: true` when checking the feature flag state:
```ruby
before_action do
# Prefer to scope it per project or user e.g.
push_frontend_feature_flag(:vim_bindings, project, default_enabled: true)
end
```
If not specified, the default feature flag type for `push_frontend_feature_flag`
is `type: development`. For all other feature flag types, you must specify the `type:`:
```ruby
before_action do
push_frontend_feature_flag(:vim_bindings, project, type: :ops)
end
```
### Feature actors
**It is strongly advised to use actors with feature flags.** Actors provide a simple
way to enable a feature flag only for a given project, group or user. This makes debugging
easier, as you can filter logs and errors for example, based on actors. This also makes it possible
to enable the feature on the `gitlab-org` or `gitlab-com` groups first, while the rest of
the users aren't impacted.
Actors also provide an easy way to do a percentage rollout of a feature in a sticky way.
If a 1% rollout enabled a feature for a specific actor, that actor will continue to have the feature enabled at
10%, 50%, and 100%.
GitLab currently supports the following models as feature flag actors:
- `User`
- `Project`
- `Group`
The actor is a second parameter of the `Feature.enabled?` call. The
same actor type must be used consistently for all invocations of `Feature.enabled?`.
```ruby
Feature.enabled?(:feature_flag, project)
Feature.enabled?(:feature_flag, group)
Feature.enabled?(:feature_flag, user)
```
#### Selectively disable by actor
By default you cannot selectively disable a feature flag by actor.
```shell
# This will not work how you would expect.
/chatops run feature set some_feature true
/chatops run feature set --project=gitlab-org/gitlab some_feature false
```
However, if you add two feature flags, you can write your conditional statement in such a way that the equivalent selective disable is possible.
```ruby
Feature.enabled?(:a_feature, project) && Feature.disabled?(:a_feature_override, project)
```
```shell
# This will enable a feature flag globally, except for gitlab-org/gitlab
/chatops run feature set a_feature true
/chatops run feature set --project=gitlab-org/gitlab a_feature_override true
```
### Enable additional objects as actors
To use feature gates based on actors, the model needs to respond to
`flipper_id`. For example, to enable for the Foo model:
```ruby
class Foo < ActiveRecord::Base
include FeatureGate
end
```
Only models that `include FeatureGate` or expose `flipper_id` method can be
used as an actor for `Feature.enabled?`.
### Feature flags for licensed features
You can't use a feature flag with the same name as a licensed feature name, because
it would cause a naming collision. This was [widely discussed and removed](https://gitlab.com/gitlab-org/gitlab/-/issues/259611)
because it is confusing.
To check for licensed features, add a dedicated feature flag under a different name
and check it explicitly, for example:
```ruby
Feature.enabled?(:licensed_feature_feature_flag, project) &&
project.feature_available?(:licensed_feature)
```
### Feature groups
Feature groups must be defined statically in `lib/feature.rb` (in the
`.register_feature_groups` method), but their implementation can obviously be
dynamic (querying the DB, for example).
Once defined in `lib/feature.rb`, you can to activate a
feature for a given feature group via the [`feature_group` parameter of the features API](../../api/features.md#set-or-create-a-feature)
### Enabling a feature flag locally (in development)
In the rails console (`rails c`), enter the following command to enable a feature flag:
```ruby
Feature.enable(:feature_flag_name)
```
Similarly, the following command disables a feature flag:
```ruby
Feature.disable(:feature_flag_name)
```
You can also enable a feature flag for a given gate:
```ruby
Feature.enable(:feature_flag_name, Project.find_by_full_path("root/my-project"))
```
### Removing a feature flag locally (in development)
When manually enabling or disabling a feature flag from the Rails console, its default value gets overwritten.
This can cause confusion when changing the flag's `default_enabled` attribute.
To reset the feature flag to the default status, you can remove it in the rails console (`rails c`)
as follows:
```ruby
Feature.remove(:feature_flag_name)
```
## Feature flags in tests
Introducing a feature flag into the codebase creates an additional code path that should be tested.
It is strongly advised to test all code affected by a feature flag, both when **enabled** and **disabled**
to ensure the feature works properly.
When using the testing environment, all feature flags are enabled by default.
WARNING:
This does not apply to end-to-end (QA) tests, which [do not disable feature flags by default](#end-to-end-qa-tests). There is a different [process for using feature flags in end-to-end tests](../testing_guide/end_to_end/feature_flags.md).
To disable a feature flag in a test, use the `stub_feature_flags`
helper. For example, to globally disable the `ci_live_trace` feature
flag in a test:
```ruby
stub_feature_flags(ci_live_trace: false)
Feature.enabled?(:ci_live_trace) # => false
```
If you wish to set up a test where a feature flag is enabled only
for some actors and not others, you can specify this in options
passed to the helper. For example, to enable the `ci_live_trace`
feature flag for a specific project:
```ruby
project1, project2 = build_list(:project, 2)
# Feature will only be enabled for project1
stub_feature_flags(ci_live_trace: project1)
Feature.enabled?(:ci_live_trace) # => false
Feature.enabled?(:ci_live_trace, project1) # => true
Feature.enabled?(:ci_live_trace, project2) # => false
```
The behavior of FlipperGate is as follows:
1. You can enable an override for a specified actor to be enabled.
1. You can disable (remove) an override for a specified actor,
falling back to the default state.
1. There's no way to model that you explicitly disabled a specified actor.
```ruby
Feature.enable(:my_feature)
Feature.disable(:my_feature, project1)
Feature.enabled?(:my_feature) # => true
Feature.enabled?(:my_feature, project1) # => true
Feature.disable(:my_feature2)
Feature.enable(:my_feature2, project1)
Feature.enabled?(:my_feature2) # => false
Feature.enabled?(:my_feature2, project1) # => true
```
### `have_pushed_frontend_feature_flags`
Use `have_pushed_frontend_feature_flags` to test if [`push_frontend_feature_flag`](#frontend)
has added the feature flag to the HTML.
For example,
```ruby
stub_feature_flags(value_stream_analytics_path_navigation: false)
visit group_analytics_cycle_analytics_path(group)
expect(page).to have_pushed_frontend_feature_flags(valueStreamAnalyticsPathNavigation: false)
```
### `stub_feature_flags` vs `Feature.enable*`
It is preferred to use `stub_feature_flags` to enable feature flags
in the testing environment. This method provides a simple and well described
interface for simple use cases.
However, in some cases more complex behavior needs to be tested,
like percentage rollouts of feature flags. This can be done using
`.enable_percentage_of_time` or `.enable_percentage_of_actors`:
```ruby
# Good: feature needs to be explicitly disabled, as it is enabled by default if not defined
stub_feature_flags(my_feature: false)
stub_feature_flags(my_feature: true)
stub_feature_flags(my_feature: project)
stub_feature_flags(my_feature: [project, project2])
# Bad
Feature.enable(:my_feature_2)
# Good: enable my_feature for 50% of time
Feature.enable_percentage_of_time(:my_feature_3, 50)
# Good: enable my_feature for 50% of actors/gates/things
Feature.enable_percentage_of_actors(:my_feature_4, 50)
```
Each feature flag that has a defined state is persisted
during test execution time:
```ruby
Feature.persisted_names.include?('my_feature') => true
Feature.persisted_names.include?('my_feature_2') => true
Feature.persisted_names.include?('my_feature_3') => true
Feature.persisted_names.include?('my_feature_4') => true
```
### Stubbing actor
When you want to enable a feature flag for a specific actor only,
you can stub its representation. A gate that is passed
as an argument to `Feature.enabled?` and `Feature.disabled?` must be an object
that includes `FeatureGate`.
In specs you can use the `stub_feature_flag_gate` method that allows you to
quickly create a custom actor:
```ruby
gate = stub_feature_flag_gate('CustomActor')
stub_feature_flags(ci_live_trace: gate)
Feature.enabled?(:ci_live_trace) # => false
Feature.enabled?(:ci_live_trace, gate) # => true
```
You can also disable a feature flag for a specific actor:
```ruby
gate = stub_feature_flag_gate('CustomActor')
stub_feature_flags(ci_live_trace: false, thing: gate)
```
### Controlling feature flags engine in tests
Our Flipper engine in the test environment works in a memory mode `Flipper::Adapters::Memory`.
`production` and `development` modes use `Flipper::Adapters::ActiveRecord`.
You can control whether the `Flipper::Adapters::Memory` or `ActiveRecord` mode is being used.
#### `stub_feature_flags: true` (default and preferred)
In this mode Flipper is configured to use `Flipper::Adapters::Memory` and mark all feature
flags to be on-by-default and persisted on a first use. This overwrites the `default_enabled:`
of `Feature.enabled?` and `Feature.disabled?` returning always `true` unless feature flag
is persisted.
Make sure behavior under feature flag doesn't go untested in some non-specific contexts.
### `stub_feature_flags: false`
This disables a memory-stubbed flipper, and uses `Flipper::Adapters::ActiveRecord`
a mode that is used by `production` and `development`.
You should use this mode only when you really want to tests aspects of Flipper
with how it interacts with `ActiveRecord`.
### End-to-end (QA) tests
Toggling feature flags works differently in end-to-end (QA) tests. The end-to-end test framework does not have direct access to
Rails or the database, so it can't use Flipper. Instead, it uses [the public API](../../api/features.md#set-or-create-a-feature). Each end-to-end test can [enable or disable a feature flag during the test](../testing_guide/end_to_end/feature_flags.md). Alternatively, you can enable or disable a feature flag before one or more tests when you [run them from your GitLab repository's `qa` directory](https://gitlab.com/gitlab-org/gitlab/tree/master/qa#running-tests-with-a-feature-flag-enabled-or-disabled), or if you [run the tests via GitLab QA](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#running-tests-with-a-feature-flag-enabled).
[As noted above, feature flags are not enabled by default in end-to-end tests.](#feature-flags-in-tests)
This means that end-to-end tests will run with feature flags in the default state implemented in the source
code, or with the feature flag in its current state on the GitLab instance under test, unless the
test is written to enable/disable a feature flag explicitly.
When a feature flag is changed on Staging or on GitLab.com, a Slack message will be posted to the `#qa-staging` or `#qa-production` channels to inform
the pipeline triage DRI so that they can more easily determine if any failures are related to a feature flag change. However, if you are working on a change you can
help to avoid unexpected failures by [confirming that the end-to-end tests pass with a feature flag enabled.](../testing_guide/end_to_end/feature_flags.md#confirming-that-end-to-end-tests-pass-with-a-feature-flag-enabled)

View File

@ -1,177 +1,8 @@
---
type: reference, dev
stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
redirect_to: 'https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/'
---
# Feature flags process
This document was moved to [another location](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/).
## Feature flags for user applications
This document only covers feature flags used in the development of GitLab
itself. Feature flags in deployed user applications can be found at
[Feature Flags feature documentation](../../operations/feature_flags.md).
## Feature flags in GitLab development
The following highlights should be considered when deciding if feature flags
should be leveraged:
- By default, the feature flags should be **off**.
- Feature flags should remain in the codebase for as short period as possible
to reduce the need for feature flag accounting.
- The person operating with feature flags is responsible for clearly communicating
the status of a feature behind the feature flag with responsible stakeholders. The
issue description should be updated with the feature flag name and whether it is
defaulted on or off as soon it is evident that a feature flag is needed.
- Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable must have the
~"feature flag" label assigned.
- When development of a feature will be spread across multiple merge
requests, you can use the following workflow:
1. [Create a new feature flag](development.md#create-a-new-feature-flag)
which is **off** by default, in the first merge request which uses the flag.
Flags [should not be added separately](development.md#risk-of-a-broken-master-main-branch).
1. Submit incremental changes via one or more merge requests, ensuring that any
new code added can only be reached if the feature flag is **on**.
You can keep the feature flag enabled on your local GDK during development.
1. When the feature is ready to be tested, enable the feature flag for
a specific project and ensure that there are no issues with the implementation.
1. When the feature is ready to be announced, create a merge request that adds
documentation about the feature, including [documentation for the feature flag itself](../documentation/feature_flags.md),
and a changelog entry. In the same merge request either flip the feature flag to
be **on by default** or remove it entirely in order to enable the new behavior.
One might be tempted to think that feature flags will delay the release of a
feature by at least one month (= one release). This is not the case. A feature
flag does not have to stick around for a specific amount of time
(e.g. at least one release), instead they should stick around until the feature
is deemed stable. Stable means it works on GitLab.com without causing any
problems, such as outages.
Please also read the [development guide for feature flags](development.md).
### Including a feature behind feature flag in the final release
In order to build a final release and present the feature for self-managed
users, the feature flag should be at least defaulted to **on**. If the feature
is deemed stable and there is confidence that removing the feature flag is safe,
consider removing the feature flag altogether. It's _strongly_ recommended that
the feature flag is [enabled **globally** on **production**](controls.md#enabling-a-feature-for-gitlabcom) for **at least one day**
before making this decision. Unexpected bugs are sometimes discovered during this period.
The process for enabling features that are disabled by default can take 5-6 days
from when the merge request is first reviewed to when the change is deployed to
GitLab.com. However, it is recommended to allow 10-14 days for this activity to
account for unforeseen problems.
Feature flags must be [documented according to their state (enabled/disabled)](../documentation/feature_flags.md),
and when the state changes, docs **must** be updated accordingly.
NOTE:
Take into consideration that such action can make the feature available on
GitLab.com shortly after the change to the feature flag is merged.
Changing the default state or removing the feature flag has to be done before
the 22nd of the month, _at least_ 3-4 working days before, in order for the change
to be included in the final self-managed release.
In addition to this, the feature behind feature flag should:
- Run in all GitLab.com environments for a sufficient period of time. This time
period depends on the feature behind the feature flag, but as a general rule of
thumb 2-4 working days should be sufficient to gather enough feedback.
- The feature should be exposed to all users within the GitLab.com plan during
the above mentioned period of time. Exposing the feature to a smaller percentage
or only a group of users might not expose a sufficient amount of information to aid in
making a decision on feature stability.
While rare, release managers may decide to reject picking or revert a change in
a stable branch, even when feature flags are used. This might be necessary if
the changes are deemed problematic, too invasive, or there simply isn't enough
time to properly measure how the changes behave on GitLab.com.
### The cost of feature flags
When reading the above, one might be tempted to think this procedure is going to
add a lot of work. Fortunately, this is not the case, and we'll show why. For
this example we'll specify the cost of the work to do as a number, ranging from
0 to infinity. The greater the number, the more expensive the work is. The cost
does _not_ translate to time, it's just a way of measuring complexity of one
change relative to another.
Let's say we are building a new feature, and we have determined that the cost of
this is 10. We have also determined that the cost of adding a feature flag check
in a variety of places is 1. If we do not use feature flags, and our feature
works as intended, our total cost is 10. This however is the best case scenario.
Optimizing for the best case scenario is guaranteed to lead to trouble, whereas
optimizing for the worst case scenario is almost always better.
To illustrate this, let's say our feature causes an outage, and there's no
immediate way to resolve it. This means we'd have to take the following steps to
resolve the outage:
1. Revert the release.
1. Perform any cleanups that might be necessary, depending on the changes that
were made.
1. Revert the commit, ensuring the "master" branch remains stable. This is
especially necessary if solving the problem can take days or even weeks.
1. Pick the revert commit into the appropriate stable branches, ensuring we
don't block any future releases until the problem is resolved.
As history has shown, these steps are time consuming, complex, often involve
many developers, and worst of all: our users will have a bad experience using
GitLab.com until the problem is resolved.
Now let's say that all of this has an associated cost of 10. This means that in
the worst case scenario, which we should optimize for, our total cost is now 20.
If we had used a feature flag, things would have been very different. We don't
need to revert a release, and because feature flags are disabled by default we
don't need to revert and pick any Git commits. In fact, all we have to do is
disable the feature, and in the worst case, perform cleanup. Let's say that
the cost of this is 2. In this case, our best case cost is 11: 10 to build the
feature, and 1 to add the feature flag. The worst case cost is now 13:
- 10 to build the feature.
- 1 to add the feature flag.
- 2 to disable and clean up.
Here we can see that in the best case scenario the work necessary is only a tiny
bit more compared to not using a feature flag. Meanwhile, the process of
reverting our changes has been made significantly and reliably cheaper.
In other words, feature flags do not slow down the development process. Instead,
they speed up the process as managing incidents now becomes _much_ easier. Once
continuous deployments are easier to perform, the time to iterate on a feature
is reduced even further, as you no longer need to wait weeks before your changes
are available on GitLab.com.
### The benefits of feature flags
It may seem like feature flags are configuration, which goes against our [convention-over-configuration](https://about.gitlab.com/handbook/product/product-principles/#convention-over-configuration)
principle. However, configuration is by definition something that is user-manageable.
Feature flags are not intended to be user-editable. Instead, they are intended as a tool for Engineers
and Site Reliability Engineers to use to de-risk their changes. Feature flags are the shim that gets us
to Continuous Delivery with our monorepo and without having to deploy the entire codebase on every change.
Feature flags are created to ensure that we can safely rollout our work on our terms.
If we use Feature Flags as a configuration, we are doing it wrong and are indeed in violation of our
principles. If something needs to be configured, we should intentionally make it configuration from the
first moment.
Some of the benefits of using development-type feature flags are:
1. It enables Continuous Delivery for GitLab.com.
1. It significantly reduces Mean-Time-To-Recovery.
1. It helps engineers to monitor and reduce the impact of their changes gradually, at any scale,
allowing us to be more metrics-driven and execute good DevOps practices, [shifting some responsibility "left"](https://devops.com/why-its-time-for-site-reliability-engineering-to-shift-left/).
1. Controlled feature rollout timing: without feature flags, we would need to wait until a specific
deployment was complete (which at GitLab could be at any time).
1. Increased psychological safety: when a feature flag is used, an engineer has the confidence that if anything goes wrong they can quickly disable the code and minimize the impact of a change that might be risky.
1. Improved throughput: when a change is less risky because a flag exists, theoretical tests about
scalability can potentially become unnecessary or less important. This allows an engineer to
potentially test a feature on a small project, monitor the impact, and proceed. The alternative might
be to build complex benchmarks locally, or on staging, or on another GitLab deployment, which has a
large impact on the time it can take to build and release a feature.
<!-- This redirect file can be deleted after 2021-06-01. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -459,7 +459,7 @@ Performance deficiencies should be addressed right away after we merge initial
changes.
Read more about when and how feature flags should be used in
[Feature flags in GitLab development](feature_flags/process.md#feature-flags-in-gitlab-development).
[Feature flags in GitLab development](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle#feature-flags-in-gitlab-development).
## Storage

View File

@ -16,18 +16,18 @@ yarn clean
## Creating feature flags in development
The process for creating a feature flag is the same as [enabling a feature flag in development](../feature_flags/development.md#enabling-a-feature-flag-locally-in-development).
The process for creating a feature flag is the same as [enabling a feature flag in development](../feature_flags/index.md#enabling-a-feature-flag-locally-in-development).
Your feature flag can now be:
- [Made available to the frontend](../feature_flags/development.md#frontend) via the `gon`
- Queried in [tests](../feature_flags/development.md#feature-flags-in-tests)
- [Made available to the frontend](../feature_flags/index.md#frontend) via the `gon`
- Queried in [tests](../feature_flags/index.md#feature-flags-in-tests)
- Queried in HAML templates and Ruby files via the `Feature.enabled?(:my_shiny_new_feature_flag)` method
### More on feature flags
- [Deleting a feature flag](../../api/features.md#delete-a-feature)
- [Manage feature flags](../feature_flags/process.md)
- [Manage feature flags](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle)
- [Feature flags API](../../api/features.md)
## Running tests locally

View File

@ -122,7 +122,7 @@ There are usually 2 phases for the MVC:
When implementing a new package manager, it is tempting to create one large merge request containing all of the
necessary endpoints and services necessary to support basic usage. Instead, put the
API endpoints behind a [feature flag](feature_flags/development.md) and
API endpoints behind a [feature flag](feature_flags/index.md) and
submit each endpoint or behavior (download, upload, etc) in a different merge request to shorten the review
process.

View File

@ -75,7 +75,7 @@ To actually use it, you need to enable measuring for the desired service by enab
### Enabling measurement using feature flags
In the following example, the `:gitlab_service_measuring_projects_import_service`
[feature flag](feature_flags/development.md#enabling-a-feature-flag-locally-in-development) is used to enable the measuring feature
[feature flag](feature_flags/index.md#enabling-a-feature-flag-locally-in-development) is used to enable the measuring feature
for `Projects::ImportService`.
From ChatOps:

View File

@ -545,7 +545,7 @@ end
### Feature flags in tests
This section was moved to [developing with feature flags](../feature_flags/development.md).
This section was moved to [developing with feature flags](../feature_flags/index.md).
### Pristine test environments

View File

@ -18,8 +18,8 @@ Please be sure to include the tag `:requires_admin` so that the test can be skip
where admin access is not available.
WARNING:
You are strongly advised to [enable feature flags only for a group, project, user](../../feature_flags/development.md#feature-actors),
or [feature group](../../feature_flags/development.md#feature-groups). This makes it possible to
You are strongly advised to [enable feature flags only for a group, project, user](../../feature_flags/index.md#feature-actors),
or [feature group](../../feature_flags/index.md#feature-groups). This makes it possible to
test a feature in a shared environment without affecting other users.
For example, the code below would enable a feature flag named `:feature_flag_name` for the project

View File

@ -3,6 +3,5 @@ redirect_to: 'usage_ping/index.md'
---
This document was moved to [another location](usage_ping/index.md).
<!-- This redirect file can be deleted after <2021-05-23>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -947,7 +947,7 @@ Each aggregate definition includes following parts:
relay on the same data source. Additional data source requirements are described in the
[Database sourced aggregated metrics](#database-sourced-aggregated-metrics) and
[Redis sourced aggregated metrics](#redis-sourced-aggregated-metrics) sections.
- `feature_flag`: Name of [development feature flag](../feature_flags/development.md#development-type)
- `feature_flag`: Name of [development feature flag](../feature_flags/index.md#development-type)
that is checked before metrics aggregation is performed. Corresponding feature flag
should have `default_enabled` attribute set to `false`. The `feature_flag` attribute
is optional and can be omitted. When `feature_flag` is missing, no feature flag is checked.

View File

@ -61,6 +61,6 @@ The following analytics features are available at the project level:
- [Insights](../project/insights/index.md). **(ULTIMATE)**
- [Issue](../group/issues_analytics/index.md). **(PREMIUM)**
- [Merge Request](merge_request_analytics.md), enabled with the `project_merge_request_analytics`
[feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-locally-in-development). **(PREMIUM)**
[feature flag](../../development/feature_flags/index.md#enabling-a-feature-flag-locally-in-development). **(PREMIUM)**
- [Repository](repository_analytics.md). **(FREE)**
- [Value Stream](value_stream_analytics.md). **(FREE)**

View File

@ -242,7 +242,7 @@ a [beta feature](https://about.gitlab.com/handbook/product/#beta).
For a group, you can view how many merge requests, issues, and members were created in the last 90 days.
These Group Activity Analytics can be enabled with the `group_activity_analytics` [feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-locally-in-development).
These Group Activity Analytics can be enabled with the `group_activity_analytics` [feature flag](../../development/feature_flags/index.md#enabling-a-feature-flag-locally-in-development).
![Recent Group Activity](img/group_activity_analytics_v13_10.png)

View File

@ -103,7 +103,6 @@ With this option enabled, users must go through your group's GitLab single sign-
However, users are not prompted to sign in through SSO on each visit. GitLab checks whether a user
has authenticated through SSO. If it's been more than 1 day since the last sign-in, GitLab
prompts the user to sign in again through SSO.
You can see more information about how long a session is valid in our [user profile documentation](../../profile/#why-do-i-keep-getting-signed-out).
We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab/-/issues/9152).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -53,7 +53,7 @@ PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name?sta
Provide the file context in the request body.
Example request:
Example request using a personal access token:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" \
@ -69,6 +69,22 @@ Example response:
}
```
Example request using a deploy token:
```shell
curl --header "DEPLOY-TOKEN: <deploy_token>" \
--upload-file path/to/file.txt \
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt?status=hidden"
```
Example response:
```json
{
"message":"201 Created"
}
```
## Download package file
Download a package file.

View File

@ -40,7 +40,7 @@ On your profile page, you can see the following information:
- Contributed projects: [projects](../project/index.md) you contributed to
- Personal projects: your personal projects (respecting the project's visibility level)
- Starred projects: projects you starred
- Snippets: your personal code [snippets](../snippets.md#personal-snippets)
- Snippets: your personal code [snippets](../snippets.md)
- Followers: people [following](../index.md#user-activity) you
- Following: people you are [following](../index.md#user-activity)

View File

@ -130,6 +130,22 @@ To pull packages in the GitLab package registry, you must:
1. For the [package type of your choice](../../packages/index.md), follow the
authentication instructions for deploy tokens.
Example request publishing a generic package using a deploy token:
```shell
curl --header "DEPLOY-TOKEN: <deploy_token>" \
--upload-file path/to/file.txt \
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt?status=hidden"
```
Example response:
```json
{
"message":"201 Created"
}
```
### Push or upload packages
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213566) in GitLab 13.0.

View File

@ -137,7 +137,7 @@ You can mark two issues as related, so that when viewing one, the other is alway
listed in its [Related Issues](related_issues.md) section. This can help display important
context, such as past work, dependencies, or duplicates.
Users on [GitLab Starter, GitLab Bronze, and higher tiers](https://about.gitlab.com/pricing/), can
Users of [GitLab Premium](https://about.gitlab.com/pricing/) or higher can
also mark issues as blocking or blocked by another issue.
### Crosslinking issues
@ -162,9 +162,9 @@ Up to five similar issues, sorted by most recently updated, are displayed below
### Health status **(ULTIMATE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4 and later.
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in GitLab Ultimate 12.10.
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in GitLab Ultimate 13.4 and later.
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in GitLab Ultimate 13.6.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7.
To help you track issue statuses, you can assign a status to each issue.

View File

@ -44,6 +44,7 @@ Compliance framework labels do not affect your project settings.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276221) in GitLab 13.9.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It can be enabled or disabled for a single group
> - 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-custom-compliance-frameworks). **(PREMIUM)**
@ -329,11 +330,11 @@ can enable it.
To enable it:
```ruby
Feature.enable(:ff_custom_compliance_frameworks)
Feature.enable(:ff_custom_compliance_frameworks, Group.find(<group id>))
```
To disable it:
```ruby
Feature.disable(:ff_custom_compliance_frameworks)
Feature.disable(:ff_custom_compliance_frameworks, Group.find(<group id>))
```

View File

@ -7,83 +7,98 @@ type: reference
# Snippets **(FREE)**
With GitLab Snippets you can store and share bits of code and text with other users.
With GitLab snippets, you can store and share bits of code and text with other users.
You can [comment on](#comment-on-snippets), [clone](#clone-snippets), and
[use version control](#versioned-snippets) in snippets. They can
[contain multiple files](#add-or-remove-multiple-files). They also support
[syntax highlighting](#filenames), [embedding](#embed-snippets), [downloading](#download-snippets),
and you can maintain your snippets with the [snippets API](../api/snippets.md).
![GitLab Snippet](img/gitlab_snippet_v13_0.png)
GitLab provides two types of snippets:
Snippets can be maintained using [snippets API](../api/snippets.md).
- **Personal snippets**: Created independent of any project.
You can set a [visibility level](../public_access/public_access.md)
for your snippet: public, internal, or private.
- **Project snippets**: Always related to a specific project.
Project snippets can be visible publicly or to only group members.
There are two types of snippets:
## Create snippets
- Personal snippets.
- Project snippets.
You can create snippets in multiple ways, depending on whether you want to create a personal or project snippet:
## Personal snippets
1. Select the kind of snippet you want to create:
- **To create a personal snippet**:
- *If you're on a project's page,* select the plus icon (**{plus-square-o}**)
in the top navigation bar, then select **New snippet** from the **GitLab** (for GitLab.com)
or **Your Instance** (self-managed) section of the same dropdown menu.
- *For all other pages,* select the plus icon (**{plus-square-o}**)
in the top navigation bar, then select **New snippet** from the dropdown menu.
- **To create a project snippet**: Go to your project's page. Select the plus icon
(**{plus-square-o}**), then select **New snippet** from the **This project** section
of the dropdown menu.
1. Add a **Title** and **Description**.
1. Name your **File** with an appropriate extension, such as `example.rb` or `index.html`.
Filenames with appropriate extensions display [syntax highlighting](#filenames).
Failure to add a filename can cause a known
[copy-pasting bug](https://gitlab.com/gitlab-org/gitlab/-/issues/22870). If you don't provide a filename, GitLab [creates a name for you](#filenames).
1. (Optional) Add [multiple files](#add-or-remove-multiple-files) to your snippet.
1. Select a visibility level, and select **Create snippet**.
Personal snippets are not related to any project and can be created completely
independently. There are 3 visibility levels that can be set, public, internal
and private. See [Public access](../public_access/public_access.md) for more information.
After you create a snippet, you can still [add more files to it](#add-or-remove-multiple-files).
In GitLab versions 13.0 and later, snippets are [versioned by default](#versioned-snippets).
## Project snippets
## Discover snippets
Project snippets are always related to a specific project.
See [Project features](project/index.md#project-features) for more information.
To discover all snippets visible to you in GitLab, you can:
## Create a snippet
- **View all snippets visible to you**: In the top navigation bar of your GitLab
instance, go to **More > Snippets** to view your snippets dashboard.
- **Visit [GitLab snippets](http://snippets.gitlab.com/)** for your snippets on GitLab.com.
- **Explore all public snippets**: In the top navigation bar of your GitLab
instance, go to **More > Snippets** and select **Explore snippets** to view
[all public snippets](https://gitlab.com/explore/snippets).
- **View a project's snippets**: In your project,
go to **Snippets**.
To create a personal snippet, click the plus icon (**{plus-square-o}**)
on the top navigation and select **New snippet** from the dropdown menu:
## Change default visibility of snippets
![New personal snippet from non-project pages](img/new_personal_snippet_v12_10.png)
Project snippets are enabled and available by default. To change their
default visibility:
If you're on a project's page but you want to create a new personal snippet,
click the plus icon (**{plus-square-o}**) and select **New snippet** from the
lower part of the dropdown (**GitLab** on GitLab.com; **Your Instance** on
self-managed instances):
1. In your project,
go to **Settings**.
1. Expand the **Visibility, project features, permissions** section, and scroll to **Snippets**.
1. Toggle the default visibility, and select whether snippets can be viewed by
everyone, or only project members.
1. Select **Save changes**.
![New personal snippet from project pages](img/new_personal_snippet_from_project_v12_10.png)
To create a project snippet, navigate to your project's page and click the
plus icon (**{plus-square-o}**), then select **New snippet** from the upper
part of the dropdown (**This project**).
![New personal snippet from project pages](img/new_project_snippet_from_project_v12_10.png)
From there, add the **Title**, **Description**, and a **File** name with the
appropriate extension (for example, `example.rb`, `index.html`).
WARNING:
Make sure to add the filename to get code highlighting and to avoid this
[copy-pasting bug](https://gitlab.com/gitlab-org/gitlab/-/issues/22870).
## Versioned Snippets
## Versioned snippets
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/239) in GitLab 13.0.
Starting in 13.0, snippets (both personal and project snippets)
In GitLab versions 13.0 and later, snippets (both personal and project snippets)
have version control enabled by default.
This means that all snippets get their own underlying repository initialized with
a `master` branch at the moment the snippet is created. Whenever a change to the snippet is saved, a
new commit to the `master` branch is recorded. Commit messages are automatically
generated. The snippet's repository has only one branch (`master`) by default, deleting
it or creating other branches is not supported.
a default branch at the moment the snippet is created. Whenever a change to the snippet is saved, a
new commit to the default branch is recorded. Commit messages are automatically
generated. The snippet's repository has only one branch. You can't delete this branch,
or create other branches.
Existing snippets are automatically migrated in 13.0. Their current
content is saved as the initial commit to the snippets' repository.
Existing snippets were automatically migrated in 13.0. Their current
content was saved as the initial commit to the snippets' repository.
### Filenames
## Filenames
Snippets support syntax highlighting based on the filename and
extension provided for them. While you can submit a snippet
without specifying a filename and extension, it needs a valid name so the
without a filename and extension, it needs a valid name so the
content can be created as a file in the snippet's repository.
If you don't attribute a filename and extension to a snippet,
If you don't give a snippet a filename and extension,
GitLab adds a filename in the format `snippetfile<x>.txt`
where `<x>` represents a number added to the file, starting with 1. This
number increments when more snippets without an attributed
filename are added.
number increments if you add more unnamed snippets.
When upgrading from an earlier version of GitLab to 13.0, existing snippets
without a supported filename are renamed to a compatible format. For
@ -92,64 +107,96 @@ changed to `http-a-weird-filename-me` to be included in the snippet's
repository. As snippets are stored by ID, changing their filenames breaks
direct or embedded links to the snippet.
### Multiple files by Snippet
## Add or remove multiple files
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2829) in GitLab 13.5.
GitLab Snippets support multiple files in one single snippet. This is helpful
when your code snippet is composed of multiple parts or when they relate
to a certain context. For example:
A single snippet can support up to 10 files, which helps keep related files together, such as:
- A snippet that includes a script and its output.
- A snippet that includes HTML, CSS, and JS code.
- A snippet that includes HTML, CSS, and JavaScript code.
- A snippet with a `docker-compose.yml` file and its associated `.env` file.
- A `gulpfile.js` file coupled with a `package.json` file, which together can be
- A `gulpfile.js` file and a `package.json` file, which together can be
used to bootstrap a project and manage its dependencies.
Snippets support between 1 and 10 files. They can be managed via Git (because they're [versioned](#versioned-snippets)
by a Git repository), through the [Snippets API](../api/snippets.md), or in the GitLab UI.
![Multi-file Snippet](img/gitlab_snippet_v13_5.png)
You can manage these by using Git (because they're [versioned](#versioned-snippets)
by a Git repository), through the [Snippets API](../api/snippets.md), and in the GitLab UI.
To add a new file to your snippet through the GitLab UI:
1. Go to your snippet in the GitLab UI.
1. Click **Edit** in the top right.
1. Select **Edit** in the top right corner.
1. Select **Add another file**.
1. Add your content to the file in the form fields provided.
1. Click **Save changes**.
1. Select **Save changes**.
To delete a file from your snippet through the GitLab UI:
1. Go to your snippet in the GitLab UI.
1. Click **Edit** in the top right.
1. Select **Edit** in the top right corner.
1. Select **Delete file** alongside the filename of each file
you wish to delete.
1. Click **Save changes**.
1. Select **Save changes**.
### Cloning snippets
## Clone snippets
Snippets can be cloned as a regular Git repository using SSH or HTTPS. Click the **Clone**
button above the snippet content to copy the URL of your choice.
Instead of copying a snippet to a local file, you may want to clone a snippet to
preserve its relationship with the repository, so you can receive or make updates
as needed. Select the **Clone** button on a snippet to display the URLs to clone with SSH or HTTPS:
![Clone Snippet](img/snippet_clone_button_v13_0.png)
![Clone snippet](img/snippet_clone_button_v13_0.png)
This allows you to have a local copy of the snippet's repository and make
changes as needed. You can commit those changes and push them to the remote
`master` branch.
You can commit changes to a cloned snippet, and push the changes to GitLab.
### Reduce snippets repository size
## Embed snippets
Because versioned Snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
it's recommended to keep snippets' repositories as compact as possible.
Public snippets can be shared and embedded on any website. You can reuse a GitLab snippet in multiple places, and any change to the source
is reflected in the embedded snippets. When embedded, users can download it, or view the snippet in raw format.
For more information about tools to compact repositories,
see the documentation on [reducing repository size](../user/project/repository/reducing_the_repo_size_using_git.md).
To embed a snippet:
### Limitations
1. Confirm your snippet is publicly visible:
- *If it's a project snippet*, the project must be public.
- The snippet is publicly visible.
- In **Project > Settings > Permissions**, the snippets
permissions are set to **Everyone with access**.
1. In your snippet's **Embed** section, select **Copy** to copy a one-line script you can add to any website or blog post. For example:
```html
<script src="https://gitlab.com/namespace/project/snippets/SNIPPET_ID.js"></script>
```
1. Add your script to your file.
Embedded snippets display a header that shows:
- The filename, if defined.
- The snippet size.
- A link to GitLab.
- The actual snippet content.
For example:
<script src="https://gitlab.com/gitlab-org/gitlab-foss/snippets/1717978.js"></script>
## Download snippets
You can download the raw content of a snippet. By default, they download with Linux-style line endings (`LF`). If
you want to preserve the original line endings you need to add a parameter `line_ending=raw`
(For example: `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
## Comment on snippets
With snippets, you engage in a conversation about that piece of code,
which can encourage user collaboration.
## Troubleshooting
### Snippet limitations
- Binary files are not supported.
- Creating or deleting branches is not supported. Only a default `master` branch is used.
- Creating or deleting branches is not supported. Only the default branch is used.
- Git tags are not supported in snippet repositories.
- Snippets' repositories are limited to 10 files. Attempting to push more
than 10 files results in an error.
@ -160,71 +207,10 @@ see the documentation on [reducing repository size](../user/project/repository/r
is 50 MB, by default.
- Git LFS is not supported.
## Discover snippets
### Reduce snippets repository size
There are two main ways of how you can discover snippets in GitLab.
Because versioned snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
it's recommended to keep snippets' repositories as compact as possible.
For exploring all snippets that are visible to you, you can go to the Snippets
dashboard of your GitLab instance via the top navigation. For GitLab.com you can
visit [GitLab Snippets](http://snippets.gitlab.com/) or navigate to an [overview]((https://gitlab.com/dashboard/snippets)) that shows snippets
you created and allows you to explore all snippets.
To discover snippets that belong to a specific project, navigate
to the Snippets page via the left side navigation on the project page.
Project snippets are enabled and available by default. You can
disable them by navigating to your project's **Settings**, expanding
**Visibility, project features, permissions** and scrolling down to
**Snippets**. From there, you can toggle to disable them or select a
different visibility level from the dropdown menu.
## Snippet comments
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/12910) in GitLab 9.2.
With GitLab Snippets you engage in a conversation about that piece of code,
encouraging user collaboration.
## Downloading snippets
You can download the raw content of a snippet.
By default snippets are downloaded with Linux-style line endings (`LF`). If
you want to preserve the original line endings you need to add a parameter `line_ending=raw`
(For example: `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
## Embedded snippets
> Introduced in GitLab 10.8.
Public snippets can not only be shared, but also embedded on any website. With
this, you can reuse a GitLab snippet in multiple places and any change to the source
is automatically reflected in the embedded snippet.
To embed a snippet, first make sure that:
- The project is public (if it's a project snippet)
- The snippet is public
- In **Project > Settings > Permissions**, the snippets permissions are
set to **Everyone with access**
After the above conditions are met, the **Embed** section appears in your
snippet. Click the **Copy** button to copy a one-line
script that you can add to any website or blog post. For example:
```html
<script src="https://gitlab.com/namespace/project/snippets/SNIPPET_ID.js"></script>
```
Here's what an embedded snippet looks like:
<script src="https://gitlab.com/gitlab-org/gitlab-foss/snippets/1717978.js"></script>
Embedded snippets are displayed with a header that shows:
- The filename, if defined.
- The snippet size.
- A link to GitLab.
- The actual snippet content.
Actions in the header enable users to see the snippet in raw format, and download it.
For more information about tools to compact repositories,
see the documentation on [reducing repository size](../user/project/repository/reducing_the_repo_size_using_git.md).

View File

@ -64,8 +64,15 @@ module API
requires :file_name, type: String, desc: 'Package file name'
end
get "gems/:file_name", requirements: FILE_NAME_REQUIREMENTS do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299283
not_found!
authorize!(:read_package, user_project)
package_file = ::Packages::PackageFile.for_rubygem_with_file_name(
user_project, params[:file_name]
).last!
track_package_event('pull_package', :rubygems)
present_carrierwave_file!(package_file.file)
end
namespace 'api/v1' do

View File

@ -52,7 +52,8 @@ cache:
- gitlab-terraform apply
when: manual
only:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
.destroy: &destroy
stage: cleanup

View File

@ -4,8 +4,12 @@ module Gitlab
module Database
module Migrations
module BackgroundMigrationHelpers
BACKGROUND_MIGRATION_BATCH_SIZE = 1_000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1_000 # Number of jobs to bulk queue at a time
BATCH_SIZE = 1_000 # Number of rows to process per job
SUB_BATCH_SIZE = 100 # Number of rows to process per sub-batch
JOB_BUFFER_SIZE = 1_000 # Number of jobs to bulk queue at a time
BATCH_CLASS_NAME = 'PrimaryKeyBatchingStrategy' # Default batch class for batched migrations
BATCH_MIN_VALUE = 1 # Default minimum value for batched migrations
BATCH_MIN_DELAY = 2.minutes.freeze # Minimum delay between batched migrations
# Bulk queues background migration jobs for an entire table, batched by ID range.
# "Bulk" meaning many jobs will be pushed at a time for efficiency.
@ -31,7 +35,7 @@ module Gitlab
# # do something
# end
# end
def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BATCH_SIZE)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
jobs = []
@ -40,7 +44,7 @@ module Gitlab
model_class.each_batch(of: batch_size) do |relation|
start_id, end_id = relation.pluck("MIN(#{table_name}.id)", "MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
if jobs.length >= JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
# We push multiple jobs at a time to reduce the time spent in
# Sidekiq/Redis operations. We're using this buffer based approach so we
@ -89,7 +93,7 @@ module Gitlab
# # do something
# end
# end
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false, primary_column_name: :id)
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false, primary_column_name: :id)
raise "#{model_class} does not have an ID column of #{primary_column_name} to use for batch ranges" unless model_class.column_names.include?(primary_column_name.to_s)
raise "#{primary_column_name} is not an integer column" unless model_class.columns_hash[primary_column_name.to_s].type == :integer
@ -127,6 +131,79 @@ module Gitlab
final_delay
end
# Creates a batched background migration for the given table. A batched migration runs one job
# at a time, computing the bounds of the next batch based on the current migration settings and the previous
# batch bounds. Each job's execution status is tracked in the database as the migration runs. The given job
# class must be present in the Gitlab::BackgroundMigration module, and the batch class (if specified) must be
# present in the Gitlab::BackgroundMigration::BatchingStrategies module.
#
# job_class_name - The background migration job class as a string
# batch_table_name - The name of the table the migration will batch over
# batch_column_name - The name of the column the migration will batch over
# job_arguments - Extra arguments to pass to the job instance when the migration runs
# job_interval - The pause interval between each job's execution, minimum of 2 minutes
# batch_min_value - The value in the column the batching will begin at
# batch_max_value - The value in the column the batching will end at, defaults to `SELECT MAX(batch_column)`
# batch_class_name - The name of the class that will be called to find the range of each next batch
# batch_size - The maximum number of rows per job
# sub_batch_size - The maximum number of rows processed per "iteration" within the job
#
#
# *Returns the created BatchedMigration record*
#
# Example:
#
# queue_batched_background_migration(
# 'CopyColumnUsingBackgroundMigrationJob',
# :events,
# :id,
# job_interval: 2.minutes,
# other_job_arguments: ['column1', 'column2'])
#
# Where the the background migration exists:
#
# class Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob
# def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, *other_args)
# # do something
# end
# end
def queue_batched_background_migration( # rubocop:disable Metrics/ParameterLists
job_class_name,
batch_table_name,
batch_column_name,
*job_arguments,
job_interval:,
batch_min_value: BATCH_MIN_VALUE,
batch_max_value: nil,
batch_class_name: BATCH_CLASS_NAME,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
job_interval = BATCH_MIN_DELAY if job_interval < BATCH_MIN_DELAY
batch_max_value ||= connection.select_value(<<~SQL)
SELECT MAX(#{connection.quote_column_name(batch_column_name)})
FROM #{connection.quote_table_name(batch_table_name)}
SQL
migration_status = batch_max_value.nil? ? :finished : :active
batch_max_value ||= batch_min_value
Gitlab::Database::BackgroundMigration::BatchedMigration.create!(
job_class_name: job_class_name,
table_name: batch_table_name,
column_name: batch_column_name,
interval: job_interval,
min_value: batch_min_value,
max_value: batch_max_value,
batch_class_name: batch_class_name,
batch_size: batch_size,
sub_batch_size: sub_batch_size,
job_arguments: job_arguments,
status: migration_status)
end
def perform_background_migration_inline?
Rails.env.test? || Rails.env.development?
end

View File

@ -15,20 +15,23 @@ module QA
element :avatar_counter_content
end
view 'app/assets/javascripts/issuable/components/csv_export_modal.vue' do
element :export_issuable_modal
end
view 'app/assets/javascripts/issuable/components/csv_import_export_buttons.vue' do
element :export_as_csv_button
element :import_issues_dropdown
element :import_from_jira_link
end
view 'app/views/projects/issues/import_csv/_button.html.haml' do
element :import_issues_button
end
view 'app/views/shared/issuable/csv_export/_modal.html.haml' do
element :export_issues_button
end
view 'app/assets/javascripts/issuable/components/csv_export_modal.vue' do
element :export_issuable_modal
end
view 'app/views/shared/issuable/_nav.html.haml' do
element :closed_issues_link
end
@ -60,7 +63,7 @@ module QA
def click_import_issues_dropdown
# When there are no issues, the image that loads causes the buttons to jump
has_loaded_all_images?
click_element(:import_issues_dropdown)
click_element(:import_issues_button)
end
def export_issues_modal

View File

@ -61,38 +61,19 @@ RSpec.describe ApplicationExperiment, :experiment do
it "tracks the assignment" do
expect(subject).to receive(:track).with(:assignment)
subject.publish(nil)
subject.publish
end
context "when inside a request cycle" do
before do
subject.context.instance_variable_set(:@request, double('Request', headers: 'true'))
end
it "pushes the experiment knowledge into the client using Gon" do
expect(Gon).to receive(:push).with({ experiment: { 'namespaced/stub' => subject.signature } }, true)
it "pushes the experiment knowledge into the client using Gon" do
expect(Gon).to receive(:push).with(
{
experiment: {
'namespaced/stub' => { # string key because it can be namespaced
experiment: 'namespaced/stub',
key: '86208ac54ca798e11f127e8b23ec396a',
variant: 'control'
}
}
},
true
)
subject.publish(nil)
end
subject.publish
end
context "when outside a request cycle" do
it "does not push to gon when outside request cycle" do
expect(Gon).not_to receive(:push)
it "handles when Gon raises exceptions (like when it can't be pushed into)" do
expect(Gon).to receive(:push).and_raise(NoMethodError)
subject.publish(nil)
end
expect { subject.publish }.not_to raise_error
end
end

View File

@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import service from '~/batch_comments/services/drafts_service';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
@ -201,6 +202,12 @@ describe('Batch comments store actions', () => {
describe('updateDraft', () => {
let getters;
service.update = jest.fn();
service.update.mockResolvedValue({ data: { id: 1 } });
const commit = jest.fn();
let context;
let params;
beforeEach(() => {
getters = {
@ -208,43 +215,43 @@ describe('Batch comments store actions', () => {
draftsPath: TEST_HOST,
},
};
});
it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', (done) => {
const commit = jest.fn();
const context = {
context = {
getters,
commit,
};
res = { id: 1 };
mock.onAny().reply(200, res);
actions
.updateDraft(context, { note: { id: 1 }, noteText: 'test', callback() {} })
.then(() => {
expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 });
})
.then(done)
.catch(done.fail);
params = { note: { id: 1 }, noteText: 'test' };
});
it('calls passed callback', (done) => {
const commit = jest.fn();
const context = {
getters,
commit,
};
afterEach(() => jest.clearAllMocks());
it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => {
return actions.updateDraft(context, { ...params, callback() {} }).then(() => {
expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 });
});
});
it('calls passed callback', () => {
const callback = jest.fn();
res = { id: 1 };
mock.onAny().reply(200, res);
return actions.updateDraft(context, { ...params, callback }).then(() => {
expect(callback).toHaveBeenCalled();
});
});
actions
.updateDraft(context, { note: { id: 1 }, noteText: 'test', callback })
.then(() => {
expect(callback).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
it('does not stringify empty position', () => {
return actions.updateDraft(context, { ...params, position: {}, callback() {} }).then(() => {
expect(service.update.mock.calls[0][1].position).toBeUndefined();
});
});
it('stringifies a non-empty position', () => {
const position = { test: true };
const expectation = JSON.stringify(position);
return actions.updateDraft(context, { ...params, position, callback() {} }).then(() => {
expect(service.update.mock.calls[0][1].position).toBe(expectation);
});
});
});

View File

@ -1,5 +1,6 @@
import { mount, createLocalVue } from '@vue/test-utils';
import { escape } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
import NoteActions from '~/notes/components/note_actions.vue';
import NoteBody from '~/notes/components/note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
@ -13,7 +14,7 @@ describe('issue_note', () => {
let wrapper;
const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
beforeEach(() => {
const createWrapper = (props = {}) => {
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@ -23,6 +24,7 @@ describe('issue_note', () => {
store,
propsData: {
note,
...props,
},
localVue,
stubs: [
@ -33,14 +35,18 @@ describe('issue_note', () => {
'multiline-comment-form',
],
});
});
};
afterEach(() => {
wrapper.destroy();
});
describe('mutiline comments', () => {
it('should render if has multiline comment', () => {
beforeEach(() => {
createWrapper();
});
it('should render if has multiline comment', async () => {
const position = {
line_range: {
start: {
@ -69,9 +75,8 @@ describe('issue_note', () => {
line,
});
return wrapper.vm.$nextTick().then(() => {
expect(findMultilineComment().text()).toEqual('Comment on lines 1 to 2');
});
await wrapper.vm.$nextTick();
expect(findMultilineComment().text()).toBe('Comment on lines 1 to 2');
});
it('should only render if it has everything it needs', () => {
@ -147,108 +152,151 @@ describe('issue_note', () => {
});
});
it('should render user information', () => {
const { author } = note;
const avatar = wrapper.find(UserAvatarLink);
const avatarProps = avatar.props();
expect(avatarProps.linkHref).toBe(author.path);
expect(avatarProps.imgSrc).toBe(author.avatar_url);
expect(avatarProps.imgAlt).toBe(author.name);
expect(avatarProps.imgSize).toBe(40);
});
it('should render note header content', () => {
const noteHeader = wrapper.find(NoteHeader);
const noteHeaderProps = noteHeader.props();
expect(noteHeaderProps.author).toEqual(note.author);
expect(noteHeaderProps.createdAt).toEqual(note.created_at);
expect(noteHeaderProps.noteId).toEqual(note.id);
});
it('should render note actions', () => {
const { author } = note;
const noteActions = wrapper.find(NoteActions);
const noteActionsProps = noteActions.props();
expect(noteActionsProps.authorId).toBe(author.id);
expect(noteActionsProps.noteId).toBe(note.id);
expect(noteActionsProps.noteUrl).toBe(note.noteable_note_url);
expect(noteActionsProps.accessLevel).toBe(note.human_access);
expect(noteActionsProps.canEdit).toBe(note.current_user.can_edit);
expect(noteActionsProps.canAwardEmoji).toBe(note.current_user.can_award_emoji);
expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit);
expect(noteActionsProps.canReportAsAbuse).toBe(true);
expect(noteActionsProps.canResolve).toBe(false);
expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path);
expect(noteActionsProps.resolvable).toBe(false);
expect(noteActionsProps.isResolved).toBe(false);
expect(noteActionsProps.isResolving).toBe(false);
expect(noteActionsProps.resolvedBy).toEqual({});
});
it('should render issue body', () => {
const noteBody = wrapper.find(NoteBody);
const noteBodyProps = noteBody.props();
expect(noteBodyProps.note).toEqual(note);
expect(noteBodyProps.line).toBe(null);
expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
expect(noteBodyProps.isEditing).toBe(false);
expect(noteBodyProps.helpPagePath).toBe('');
});
it('prevents note preview xss', (done) => {
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
const alertSpy = jest.spyOn(window, 'alert');
store.hotUpdate({
actions: {
updateNote() {},
setSelectedCommentPositionHover() {},
},
describe('rendering', () => {
beforeEach(() => {
createWrapper();
});
const noteBodyComponent = wrapper.find(NoteBody);
noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
it('should render user information', () => {
const { author } = note;
const avatar = wrapper.findComponent(UserAvatarLink);
const avatarProps = avatar.props();
setImmediate(() => {
expect(avatarProps.linkHref).toBe(author.path);
expect(avatarProps.imgSrc).toBe(author.avatar_url);
expect(avatarProps.imgAlt).toBe(author.name);
expect(avatarProps.imgSize).toBe(40);
});
it('should render note header content', () => {
const noteHeader = wrapper.findComponent(NoteHeader);
const noteHeaderProps = noteHeader.props();
expect(noteHeaderProps.author).toBe(note.author);
expect(noteHeaderProps.createdAt).toBe(note.created_at);
expect(noteHeaderProps.noteId).toBe(note.id);
});
it('should render note actions', () => {
const { author } = note;
const noteActions = wrapper.findComponent(NoteActions);
const noteActionsProps = noteActions.props();
expect(noteActionsProps.authorId).toBe(author.id);
expect(noteActionsProps.noteId).toBe(note.id);
expect(noteActionsProps.noteUrl).toBe(note.noteable_note_url);
expect(noteActionsProps.accessLevel).toBe(note.human_access);
expect(noteActionsProps.canEdit).toBe(note.current_user.can_edit);
expect(noteActionsProps.canAwardEmoji).toBe(note.current_user.can_award_emoji);
expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit);
expect(noteActionsProps.canReportAsAbuse).toBe(true);
expect(noteActionsProps.canResolve).toBe(false);
expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path);
expect(noteActionsProps.resolvable).toBe(false);
expect(noteActionsProps.isResolved).toBe(false);
expect(noteActionsProps.isResolving).toBe(false);
expect(noteActionsProps.resolvedBy).toEqual({});
});
it('should render issue body', () => {
const noteBody = wrapper.findComponent(NoteBody);
const noteBodyProps = noteBody.props();
expect(noteBodyProps.note).toBe(note);
expect(noteBodyProps.line).toBe(null);
expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
expect(noteBodyProps.isEditing).toBe(false);
expect(noteBodyProps.helpPagePath).toBe('');
});
it('prevents note preview xss', async () => {
const noteBody =
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert(1)" />';
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const noteBodyComponent = wrapper.findComponent(NoteBody);
store.hotUpdate({
actions: {
updateNote() {},
setSelectedCommentPositionHover() {},
},
});
noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {});
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toEqual(escape(noteBody));
done();
expect(wrapper.vm.note.note_html).toBe(escape(noteBody));
});
});
describe('cancel edit', () => {
it('restores content of updated note', (done) => {
beforeEach(() => {
createWrapper();
});
it('restores content of updated note', async () => {
const updatedText = 'updated note text';
store.hotUpdate({
actions: {
updateNote() {},
},
});
const noteBody = wrapper.find(NoteBody);
const noteBody = wrapper.findComponent(NoteBody);
noteBody.vm.resetAutoSave = () => {};
noteBody.vm.$emit('handleFormUpdate', updatedText, null, () => {});
wrapper.vm
.$nextTick()
.then(() => {
const noteBodyProps = noteBody.props();
await wrapper.vm.$nextTick();
let noteBodyProps = noteBody.props();
expect(noteBodyProps.note.note_html).toBe(updatedText);
noteBody.vm.$emit('cancelForm');
})
.then(() => wrapper.vm.$nextTick())
.then(() => {
const noteBodyProps = noteBody.props();
expect(noteBodyProps.note.note_html).toBe(updatedText);
expect(noteBodyProps.note.note_html).toBe(note.note_html);
})
.then(done)
.catch(done.fail);
noteBody.vm.$emit('cancelForm');
await wrapper.vm.$nextTick();
noteBodyProps = noteBody.props();
expect(noteBodyProps.note.note_html).toBe(note.note_html);
});
});
describe('formUpdateHandler', () => {
const updateNote = jest.fn();
const params = ['', null, jest.fn(), ''];
const updateActions = () => {
store.hotUpdate({
actions: {
updateNote,
setSelectedCommentPositionHover() {},
},
});
};
afterEach(() => updateNote.mockReset());
it('responds to handleFormUpdate', () => {
createWrapper();
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params);
expect(wrapper.emitted('handleUpdateNote')).toBeTruthy();
});
it('does not stringify empty position', () => {
createWrapper();
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params);
expect(updateNote.mock.calls[0][1].note.note.position).toBeUndefined();
});
it('stringifies populated position', () => {
const position = { test: true };
const expectation = JSON.stringify(position);
createWrapper({ note: { ...note, position } });
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params);
expect(updateNote.mock.calls[0][1].note.note.position).toBe(expectation);
});
});
});

View File

@ -42,20 +42,38 @@ EOF
end
context 'with Content-Length over the limit' do
it 'extracts multipart message' do
env = Rack::MockRequest.env_for("/", multipart_fixture(:text, 500_000_001))
shared_examples 'logs multipart message' do
it 'extracts multipart message' do
env = Rack::MockRequest.env_for("/", multipart_fixture(:text, length))
expect(described_class).to receive(:log_large_multipart?).and_return(true)
expect(described_class).to receive(:log_multipart_warning).and_call_original
expect(described_class).to receive(:log_warn).with({
message: 'Large multipart body detected',
path: '/',
content_length: anything,
correlation_id: anything
})
params = described_class.parse_multipart(env)
expect(described_class).to receive(:log_large_multipart?).and_return(true)
expect(described_class).to receive(:log_multipart_warning).and_call_original
expect(described_class).to receive(:log_warn).with({
message: 'Large multipart body detected',
path: '/',
content_length: anything,
correlation_id: anything
})
params = described_class.parse_multipart(env)
expect(params.keys).to include(*%w(reply fileupload))
expect(params.keys).to include(*%w(reply fileupload))
end
end
context 'from environment' do
let(:length) { 1001 }
before do
stub_env('RACK_MULTIPART_LOGGING_BYTES', 1000)
end
it_behaves_like 'logs multipart message'
end
context 'default limit' do
let(:length) { 100_000_001 }
it_behaves_like 'logs multipart message'
end
end
end

View File

@ -10,6 +10,10 @@ RSpec.describe Banzai::Filter::CustomEmojiFilter do
let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) }
let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') }
it_behaves_like 'emoji filter' do
let(:emoji_name) { ':tanuki:' }
end
it 'replaces supported name custom emoji' do
doc = filter('<p>:tanuki:</p>', project: project)
@ -17,25 +21,12 @@ RSpec.describe Banzai::Filter::CustomEmojiFilter do
expect(doc.css('gl-emoji img').size).to eq 1
end
it 'ignores non existent custom emoji' do
exp = '<p>:foo:</p>'
doc = filter(exp)
expect(doc.to_html).to eq(exp)
end
it 'correctly uses the custom emoji URL' do
doc = filter('<p>:tanuki:</p>')
expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file)
end
it 'matches with adjacent text' do
doc = filter('tanuki (:tanuki:)')
expect(doc.css('img').size).to eq 1
end
it 'matches multiple same custom emoji' do
doc = filter(':tanuki: :tanuki:')
@ -54,18 +45,6 @@ RSpec.describe Banzai::Filter::CustomEmojiFilter do
expect(doc.css('img').size).to be 0
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :tanuki:, big time.')
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'does not match emoji in a string' do
doc = filter("'2a00:tanuki:100::1'")
expect(doc.css('gl-emoji').size).to eq 0
end
it 'does not do N+1 query' do
create(:custom_emoji, name: 'party-parrot', group: group)
@ -77,10 +56,4 @@ RSpec.describe Banzai::Filter::CustomEmojiFilter do
filter('<p>:tanuki: :party-parrot:</p>')
end.not_to exceed_all_query_limit(control_count.count)
end
context 'ancestor tags' do
let(:emoji_name) { ':tanuki:' }
it_behaves_like 'ignored ancestor tags'
end
end

View File

@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::EmojiFilter do
include FilterSpecHelper
it_behaves_like 'emoji filter' do
let(:emoji_name) { ':+1:' }
end
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('gl-emoji').first.text).to eq '❤'
@ -15,12 +19,6 @@ RSpec.describe Banzai::Filter::EmojiFilter do
expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'ignores unsupported emoji' do
exp = act = '<p>:foo:</p>'
doc = filter(act)
expect(doc.to_html).to match Regexp.escape(exp)
end
it 'ignores unicode versions of trademark, copyright, and registered trademark' do
exp = act = '<p>™ © ®</p>'
doc = filter(act)
@ -65,11 +63,6 @@ RSpec.describe Banzai::Filter::EmojiFilter do
expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches with adjacent text' do
doc = filter('+1 (👍)')
expect(doc.css('gl-emoji').size).to eq 1
@ -90,12 +83,6 @@ RSpec.describe Banzai::Filter::EmojiFilter do
expect(doc.css('gl-emoji').size).to eq 6
end
it 'does not match emoji in a string' do
doc = filter("'2a00:a4c0:100::1'")
expect(doc.css('gl-emoji').size).to eq 0
end
it 'has a data-name attribute' do
doc = filter(':-1:')
expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
@ -106,21 +93,9 @@ RSpec.describe Banzai::Filter::EmojiFilter do
expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'unicode keeps whitespace intact' do
doc = filter('This deserves a 🎱, big time.')
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
context 'ancestor tags' do
let(:emoji_name) { ':see_no_evil:' }
it_behaves_like 'ignored ancestor tags'
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
context 'with enough rows to bulk queue jobs more than once' do
before do
stub_const('Gitlab::Database::Migrations::BackgroundMigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1)
stub_const('Gitlab::Database::Migrations::BackgroundMigrationHelpers::JOB_BUFFER_SIZE', 1)
end
it 'queues jobs correctly' do
@ -262,6 +262,120 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
end
end
describe '#queue_batched_background_migration' do
it 'creates the database record for the migration' do
expect do
model.queue_batched_background_migration(
'MyJobClass',
:projects,
:id,
job_interval: 5.minutes,
batch_min_value: 5,
batch_max_value: 1000,
batch_class_name: 'MyBatchClass',
batch_size: 100,
sub_batch_size: 10)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes(
job_class_name: 'MyJobClass',
table_name: 'projects',
column_name: 'id',
interval: 300,
min_value: 5,
max_value: 1000,
batch_class_name: 'MyBatchClass',
batch_size: 100,
sub_batch_size: 10,
job_arguments: %w[],
status: 'active')
end
context 'when the job interval is lower than the minimum' do
let(:minimum_delay) { described_class::BATCH_MIN_DELAY }
it 'sets the job interval to the minimum value' do
expect do
model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: minimum_delay - 1.minute)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last
expect(created_migration.interval).to eq(minimum_delay)
end
end
context 'when additional arguments are passed to the method' do
it 'saves the arguments on the database record' do
expect do
model.queue_batched_background_migration(
'MyJobClass',
:projects,
:id,
'my',
'arguments',
job_interval: 5.minutes,
batch_max_value: 1000)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes(
job_class_name: 'MyJobClass',
table_name: 'projects',
column_name: 'id',
interval: 300,
min_value: 1,
max_value: 1000,
job_arguments: %w[my arguments])
end
end
context 'when the max_value is not given' do
context 'when records exist in the database' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
it 'creates the record with the current max value' do
expect do
model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last
expect(created_migration.max_value).to eq(event3.id)
end
it 'creates the record with an active status' do
expect do
model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_active
end
end
context 'when the database is empty' do
it 'sets the max value to the min value' do
expect do
model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last
expect(created_migration.max_value).to eq(created_migration.min_value)
end
it 'creates the record with a finished status' do
expect do
model.queue_batched_background_migration('MyJobClass', :projects, :id, job_interval: 5.minutes)
end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1)
expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_finished
end
end
end
end
describe '#migrate_async' do
it 'calls BackgroundMigrationWorker.perform_async' do
expect(BackgroundMigrationWorker).to receive(:perform_async).with("Class", "hello", "world")

View File

@ -62,6 +62,21 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
describe '.for_rubygem_with_file_name' do
let_it_be(:project) { create(:project) }
let_it_be(:non_ruby_package) { create(:nuget_package, project: project, package_type: :nuget) }
let_it_be(:ruby_package) { create(:rubygems_package, project: project, package_type: :rubygems) }
let_it_be(:file_name) { 'other.gem' }
let_it_be(:non_ruby_file) { create(:package_file, :nuget, package: non_ruby_package, file_name: file_name) }
let_it_be(:gem_file1) { create(:package_file, :gem, package: ruby_package) }
let_it_be(:gem_file2) { create(:package_file, :gem, package: ruby_package, file_name: file_name) }
it 'returns the matching gem file only for ruby packages' do
expect(described_class.for_rubygem_with_file_name(project, file_name)).to contain_exactly(gem_file2)
end
end
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }

View File

@ -108,11 +108,68 @@ RSpec.describe API::RubygemPackages do
end
describe 'GET /api/v4/projects/:project_id/packages/rubygems/gems/:file_name' do
let(:url) { api("/projects/#{project.id}/packages/rubygems/gems/my_gem-1.0.0.gem") }
let_it_be(:package_name) { 'package' }
let_it_be(:version) { '0.0.1' }
let_it_be(:package) { create(:rubygems_package, project: project, name: package_name, version: version) }
let_it_be(:file_name) { "#{package_name}-#{version}.gem" }
let(:url) { api("/projects/#{project.id}/packages/rubygems/gems/#{file_name}") }
subject { get(url, headers: headers) }
it_behaves_like 'an unimplemented route'
context 'with valid project' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'Rubygems gem download' | :success
:public | :guest | true | :personal_access_token | true | 'Rubygems gem download' | :success
:public | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :personal_access_token | true | 'Rubygems gem download' | :success
:public | :guest | false | :personal_access_token | true | 'Rubygems gem download' | :success
:public | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :anonymous | false | :personal_access_token | true | 'Rubygems gem download' | :success
:private | :developer | true | :personal_access_token | true | 'Rubygems gem download' | :success
:private | :guest | true | :personal_access_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :anonymous | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:public | :developer | true | :job_token | true | 'Rubygems gem download' | :success
:public | :guest | true | :job_token | true | 'Rubygems gem download' | :success
:public | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :job_token | true | 'Rubygems gem download' | :success
:public | :guest | false | :job_token | true | 'Rubygems gem download' | :success
:public | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'Rubygems gem download' | :success
:private | :guest | true | :job_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | true | :deploy_token | true | 'Rubygems gem download' | :success
:public | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :deploy_token | true | 'Rubygems gem download' | :success
:private | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
end
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
end
describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems/authorize' do
@ -171,7 +228,7 @@ RSpec.describe API::RubygemPackages do
let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility: visibility.to_s)
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
@ -249,7 +306,7 @@ RSpec.describe API::RubygemPackages do
let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility: visibility.to_s)
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
RSpec.shared_examples 'emoji filter' do
it 'keeps whitespace intact' do
doc = filter("This deserves a #{emoji_name}, big time.")
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'does not match emoji in a string' do
doc = filter("'2a00:a4c0#{emoji_name}:1'")
expect(doc.css('gl-emoji')).to be_empty
end
it 'ignores non existent/unsupported emoji' do
exp = '<p>:foo:</p>'
doc = filter(exp)
expect(doc.to_html).to eq(exp)
end
it 'matches with adjacent text' do
doc = filter("#{emoji_name.delete(':')} (#{emoji_name})")
expect(doc.css('gl-emoji').size).to eq 1
end
it 'does not match emoji in a pre tag' do
doc = filter("<p><pre>#{emoji_name}</pre></p>")
expect(doc.css('img')).to be_empty
end
it 'does not match emoji in code tag' do
doc = filter("<p><code>#{emoji_name} wow</code></p>")
expect(doc.css('img')).to be_empty
end
it 'does not match emoji in tt tag' do
doc = filter("<p><tt>#{emoji_name} yes!</tt></p>")
expect(doc.css('img')).to be_empty
end
end

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'ignored ancestor tags' do
it 'does not match emoji in a pre tag' do
doc = filter("<p><pre>#{emoji_name}</pre></p>")
expect(doc.css('img').size).to eq 0
end
it 'does not match emoji in code tag' do
doc = filter("<p><code>#{emoji_name} wow</code></p>")
expect(doc.css('img').size).to eq 0
end
it 'does not match emoji in tt tag' do
doc = filter("<p><tt>#{emoji_name} yes!</tt></p>")
expect(doc.css('img').size).to eq 0
end
end

View File

@ -66,9 +66,7 @@ RSpec.shared_examples 'boards create mutation' do
context 'when the Boards::CreateService returns an error response' do
before do
allow_next_instance_of(Boards::CreateService) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'There was an error.'))
end
params[:name] = ''
end
it 'does not create a board' do
@ -80,7 +78,7 @@ RSpec.shared_examples 'boards create mutation' do
expect(mutation_response).to have_key('board')
expect(mutation_response['board']).to be_nil
expect(mutation_response['errors'].first).to eq('There was an error.')
expect(mutation_response['errors'].first).to eq('There was an error when creating a board.')
end
end
end

View File

@ -175,3 +175,20 @@ RSpec.shared_examples 'dependency endpoint success' do |user_type, status, add_m
end
end
end
RSpec.shared_examples 'Rubygems gem download' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it 'returns the gem', :aggregate_failures do
subject
expect(response.media_type).to eq('application/octet-stream')
expect(response).to have_gitlab_http_status(status)
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
end
end