Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9d195600e6
commit
19d63dbca4
|
@ -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
|
||||
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Todos from './todos';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Todos());
|
||||
new Todos(); // eslint-disable-line no-new
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] })
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: fix stringify empty position object
|
||||
merge_request: 56037
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed error handling GraphQL API when issue board creation fails
|
||||
merge_request: 55982
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in clusters directory
|
||||
merge_request: 56205
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move to confirm variant from success in environments directory
|
||||
merge_request: 56203
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 | |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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._
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 -->
|
|
@ -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)
|
||||
|
|
|
@ -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 -->
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 -->
|
|
@ -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.
|
||||
|
|
|
@ -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)**
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>))
|
||||
```
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -52,7 +52,8 @@ cache:
|
|||
- gitlab-terraform apply
|
||||
when: manual
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
.destroy: &destroy
|
||||
stage: cleanup
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue