Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3c2841692e
commit
6aab18704a
|
@ -357,7 +357,6 @@ linters:
|
|||
- "ee/app/views/notify/unapproved_merge_request_email.html.haml"
|
||||
- "ee/app/views/oauth/geo_auth/error.html.haml"
|
||||
- "ee/app/views/projects/commits/_mirror_status.html.haml"
|
||||
- "ee/app/views/projects/jobs/_shared_runner_limit_warning.html.haml"
|
||||
- "ee/app/views/projects/merge_requests/_approvals_count.html.haml"
|
||||
- "ee/app/views/projects/merge_requests/widget/open/_geo.html.haml"
|
||||
- "ee/app/views/projects/mirrors/_mirrored_repositories_count.html.haml"
|
||||
|
|
|
@ -6,7 +6,7 @@ $brand-info: $blue-500;
|
|||
$brand-warning: $orange-500;
|
||||
$brand-danger: $red-500;
|
||||
|
||||
$border-radius-base: 3px !default;
|
||||
$border-radius-base: $gl-border-radius-base;
|
||||
|
||||
$modal-body-bg: $white;
|
||||
$input-border: $border-color;
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
- content_for :page_specific_javascripts do
|
||||
= stylesheet_link_tag 'page_bundles/xterm'
|
||||
|
||||
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
|
||||
|
||||
#js-job-vue-app{ data: jobs_data }
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
#!/usr/bin/env ruby
|
||||
#
|
||||
# Generate a feature flag entry file in the correct location.
|
||||
#
|
||||
# Automatically stages the file and amends the previous commit if the `--amend`
|
||||
# argument is used.
|
||||
|
||||
require 'optparse'
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
require 'cgi'
|
||||
|
||||
require_relative '../lib/feature/shared' unless defined?(Feature::Shared)
|
||||
|
||||
Options = Struct.new(
|
||||
:name,
|
||||
:type,
|
||||
:group,
|
||||
:ee,
|
||||
:amend,
|
||||
:dry_run,
|
||||
:force,
|
||||
:introduced_by_url,
|
||||
:rollout_issue_url
|
||||
)
|
||||
|
||||
module FeatureFlagHelpers
|
||||
Abort = Class.new(StandardError)
|
||||
Done = Class.new(StandardError)
|
||||
|
||||
def capture_stdout(cmd)
|
||||
output = IO.popen(cmd, &:read)
|
||||
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
|
||||
output
|
||||
end
|
||||
|
||||
def fail_with(message)
|
||||
raise Abort, "\e[31merror\e[0m #{message}"
|
||||
end
|
||||
end
|
||||
|
||||
class FeatureFlagOptionParser
|
||||
extend FeatureFlagHelpers
|
||||
extend ::Feature::Shared
|
||||
|
||||
class << self
|
||||
def parse(argv)
|
||||
options = Options.new
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{__FILE__} [options] <feature-flag>\n\n"
|
||||
|
||||
# Note: We do not provide a shorthand for this in order to match the `git
|
||||
# commit` interface
|
||||
opts.on('--amend', 'Amend the previous commit') do |value|
|
||||
options.amend = value
|
||||
end
|
||||
|
||||
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
|
||||
options.force = value
|
||||
end
|
||||
|
||||
opts.on('-m', '--introduced-by-url [string]', String, 'URL to Merge Request introducing Feature Flag') do |value|
|
||||
options.introduced_by_url = value
|
||||
end
|
||||
|
||||
opts.on('-i', '--rollout-issue-url [string]', String, 'URL to Issue rolling out Feature Flag') do |value|
|
||||
options.rollout_issue_url = value
|
||||
end
|
||||
|
||||
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
|
||||
options.dry_run = value
|
||||
end
|
||||
|
||||
opts.on('-g', '--group [string]', String, "The group introducing a feature flag, like: `group::apm`") do |value|
|
||||
options.group = value if value.start_with?('group::')
|
||||
end
|
||||
|
||||
opts.on('-t', '--type [string]', String, "The category of the feature flag, valid options are: #{TYPES.keys.map(&:to_s).join(', ')}") do |value|
|
||||
options.type = value.to_sym if TYPES[value.to_sym]
|
||||
end
|
||||
|
||||
opts.on('-e', '--ee', 'Generate a feature flag entry for GitLab EE') do |value|
|
||||
options.ee = value
|
||||
end
|
||||
|
||||
opts.on('-h', '--help', 'Print help message') do
|
||||
$stdout.puts opts
|
||||
raise Done.new
|
||||
end
|
||||
end
|
||||
|
||||
parser.parse!(argv)
|
||||
|
||||
unless argv.one?
|
||||
$stdout.puts parser.help
|
||||
$stdout.puts
|
||||
raise Abort, 'Feature flag name is required'
|
||||
end
|
||||
|
||||
# Name is a first name
|
||||
options.name = argv.first
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def read_group
|
||||
$stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:"
|
||||
|
||||
loop do
|
||||
$stdout.print "\n?> "
|
||||
group = $stdin.gets.strip
|
||||
group = nil if group.empty?
|
||||
return group if group.nil? || group.start_with?('group::')
|
||||
|
||||
$stderr.puts "Group needs to include `group::`"
|
||||
end
|
||||
end
|
||||
|
||||
def read_type
|
||||
$stdout.puts ">> Please specify the type of your feature flag:"
|
||||
$stdout.puts
|
||||
TYPES.each do |type, data|
|
||||
$stdout.puts "#{type.to_s.rjust(15)}#{' '*6}#{data[:description]}"
|
||||
end
|
||||
|
||||
loop do
|
||||
$stdout.print "\n?> "
|
||||
|
||||
type = $stdin.gets.strip.to_sym
|
||||
return type if TYPES[type]
|
||||
|
||||
$stderr.puts "Invalid type specified '#{type}'"
|
||||
end
|
||||
end
|
||||
|
||||
def read_issue_url(options)
|
||||
return unless TYPES.dig(options.type, :rollout_issue)
|
||||
|
||||
url = "https://gitlab.com/gitlab-org/gitlab/-/issues/new"
|
||||
title = "[Feature flag] Rollout of `#{options.name}`"
|
||||
description = File.read('.gitlab/issue_templates/Feature Flag Roll Out.md')
|
||||
description.sub!(':feature_name', options.name)
|
||||
|
||||
issue_new_url = url + "?" +
|
||||
"issue[title]=" + CGI.escape(title) + "&"
|
||||
# TODO: We should be able to pick `issueable_template`
|
||||
# + "issue[description]=" + CGI.escape(description)
|
||||
|
||||
$stdout.puts ">> Open this URL and fill the rest of details:"
|
||||
$stdout.puts issue_new_url
|
||||
$stdout.puts
|
||||
|
||||
$stdout.puts ">> Paste URL here, or enter to skip:"
|
||||
|
||||
loop do
|
||||
$stdout.print "\n?> "
|
||||
created_url = $stdin.gets.strip
|
||||
created_url = nil if created_url.empty?
|
||||
return created_url if created_url.nil? || created_url.start_with?('https://')
|
||||
|
||||
$stderr.puts "URL needs to start with https://"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class FeatureFlagCreator
|
||||
include FeatureFlagHelpers
|
||||
|
||||
attr_reader :options
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def execute
|
||||
assert_feature_branch!
|
||||
assert_name!
|
||||
assert_existing_feature_flag!
|
||||
|
||||
# Read type from $stdin unless is already set
|
||||
options.type ||= FeatureFlagOptionParser.read_type
|
||||
options.group ||= FeatureFlagOptionParser.read_group
|
||||
options.rollout_issue_url ||= FeatureFlagOptionParser.read_issue_url(options)
|
||||
|
||||
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
|
||||
$stdout.puts contents
|
||||
|
||||
unless options.dry_run
|
||||
write
|
||||
amend_commit if options.amend
|
||||
end
|
||||
|
||||
if editor
|
||||
system("#{editor} '#{file_path}'")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contents
|
||||
YAML.dump(
|
||||
'name' => options.name,
|
||||
'introduced_by_url' => options.introduced_by_url,
|
||||
'rollout_issue_url' => options.rollout_issue_url,
|
||||
'group' => options.group.to_s,
|
||||
'type' => options.type.to_s,
|
||||
'default_enabled' => false
|
||||
).strip
|
||||
end
|
||||
|
||||
def write
|
||||
FileUtils.mkdir_p(File.dirname(file_path))
|
||||
File.write(file_path, contents)
|
||||
end
|
||||
|
||||
def editor
|
||||
ENV['EDITOR']
|
||||
end
|
||||
|
||||
def amend_commit
|
||||
fail_with "git add failed" unless system(*%W[git add #{file_path}])
|
||||
|
||||
Kernel.exec(*%w[git commit --amend])
|
||||
end
|
||||
|
||||
def assert_feature_branch!
|
||||
return unless branch_name == 'master'
|
||||
|
||||
fail_with "Create a branch first!"
|
||||
end
|
||||
|
||||
def assert_existing_feature_flag!
|
||||
existing_path = all_feature_flag_names[options.name]
|
||||
return unless existing_path
|
||||
return if options.force
|
||||
|
||||
fail_with "#{existing_path} already exists! Use `--force` to overwrite."
|
||||
end
|
||||
|
||||
def assert_name!
|
||||
return if options.name.match(/\A[a-z0-9_-]+\Z/)
|
||||
|
||||
fail_with "Provide a name for the feature flag that is [a-z0-9_-]"
|
||||
end
|
||||
|
||||
def file_path
|
||||
feature_flags_paths.last
|
||||
.sub('**', options.type.to_s)
|
||||
.sub('*.yml', options.name + '.yml')
|
||||
end
|
||||
|
||||
def all_feature_flag_names
|
||||
@all_feature_flag_names ||=
|
||||
feature_flags_paths.map do |glob_path|
|
||||
Dir.glob(glob_path).map do |path|
|
||||
[File.basename(path, '.yml'), path]
|
||||
end
|
||||
end.flatten(1).to_h
|
||||
end
|
||||
|
||||
def feature_flags_paths
|
||||
paths = []
|
||||
paths << File.join('config', 'feature_flags', '**', '*.yml')
|
||||
paths << File.join('ee', 'config', 'feature_flags', '**', '*.yml') if ee?
|
||||
paths
|
||||
end
|
||||
|
||||
def ee?
|
||||
options.ee
|
||||
end
|
||||
|
||||
def branch_name
|
||||
@branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
|
||||
end
|
||||
end
|
||||
|
||||
if $0 == __FILE__
|
||||
begin
|
||||
options = FeatureFlagOptionParser.parse(ARGV)
|
||||
FeatureFlagCreator.new(options).execute
|
||||
rescue FeatureFlagHelpers::Abort => ex
|
||||
$stderr.puts ex.message
|
||||
exit 1
|
||||
rescue FeatureFlagHelpers::Done
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
# vim: ft=ruby
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix border-radius-base SCSS value
|
||||
merge_request: 35740
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update snippet statistics after project import
|
||||
merge_request: 35730
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move merge_requests_users metric to stage section
|
||||
merge_request: 35593
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
# This needs to be loaded after
|
||||
# config/initializers/0_inject_enterprise_edition_module.rb
|
||||
|
||||
Feature.register_feature_groups
|
||||
Feature.register_definitions
|
|
@ -1 +0,0 @@
|
|||
Feature.register_feature_groups
|
|
@ -908,6 +908,21 @@ result as you did at the start. For example:
|
|||
|
||||
Note that `enforced="true"` means that authentication is being enforced.
|
||||
|
||||
## Direct Git access bypassing Gitaly
|
||||
|
||||
While it is possible to access Gitaly repositories stored on disk directly with a Git client,
|
||||
it is not advisable because Gitaly is being continuously improved and changed. Theses improvements may invalidate assumptions, resulting in performance degradation, instability, and even data loss.
|
||||
|
||||
Gitaly has optimizations, such as the
|
||||
[`info/refs` advertisement cache](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/design_diskcache.md),
|
||||
that rely on Gitaly controlling and monitoring access to repositories via the
|
||||
official gRPC interface. Likewise, Praefect has optimizations, such as fault
|
||||
tolerance and distributed reads, that depend on the gRPC interface and
|
||||
database to determine repository state.
|
||||
|
||||
For these reasons, **accessing repositories directly is done at your own risk
|
||||
and is not supported**.
|
||||
|
||||
## Direct access to Git in GitLab
|
||||
|
||||
Direct access to Git uses code in GitLab known as the "Rugged patches".
|
||||
|
|
|
@ -32,7 +32,6 @@ pipelines for merge requests take precedence over the other regular pipelines.
|
|||
|
||||
To enable pipelines for merge requests:
|
||||
|
||||
- You must have maintainer [permissions](../../user/permissions.md).
|
||||
- Your repository must be a GitLab repository, not an
|
||||
[external repository](../ci_cd_for_external_repos/index.md).
|
||||
- [In GitLab 11.10 and later](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25504),
|
||||
|
|
|
@ -2465,8 +2465,6 @@ The `stop_review_app` job is **required** to have the following keywords defined
|
|||
- `when` - [reference](#when)
|
||||
- `environment:name`
|
||||
- `environment:action`
|
||||
- `stage` should be the same as the `review_app` in order for the environment
|
||||
to stop automatically when the branch is deleted
|
||||
|
||||
Additionally, both jobs should have matching [`rules`](../yaml/README.md#onlyexcept-basic)
|
||||
or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration. In the example
|
||||
|
|
|
@ -453,6 +453,28 @@ are:
|
|||
To reduce unnecessary differences between two distribution methods, Omnibus and
|
||||
CNG **should always use the same Go version**.
|
||||
|
||||
### Supporting multiple Go versions
|
||||
|
||||
Individual Golang-projects need to support multiple Go versions for the following reasons:
|
||||
|
||||
1. When a new Go release is out, we should start integrating it into the CI pipelines to verify compatibility with the new compiler.
|
||||
1. We must support the [Omnibus official Go version](#updating-go-version), which may be behind the latest minor release.
|
||||
1. When Omnibus switches Go version, we still may need to support the old one for security backports.
|
||||
|
||||
These 3 requirements may easily be satisfied by keeping support for the 3 latest minor versions of Go.
|
||||
|
||||
It's ok to drop support for the oldest Go version and support only 2 latest releases,
|
||||
if this is enough to support backports to the last 3 GitLab minor releases.
|
||||
|
||||
Example:
|
||||
|
||||
In case we want to drop support for `go 1.11` in GitLab `12.10`, we need to verify which Go versions we are using in `12.9`, `12.8`, and `12.7`.
|
||||
|
||||
We will not consider the active milestone, `12.10`, because a backport for `12.7` will be required in case of a critical security release.
|
||||
|
||||
1. If both [Omnibus and CNG](#updating-go-version) were using Go `1.12` since GitLab `12.7`, then we safely drop support for `1.11`.
|
||||
1. If Omnibus or CNG were using `1.11` in GitLab `12.7`, then we still need to keep support for Go `1.11` for easier backporting of security fixes.
|
||||
|
||||
## Secure Team standards and style guidelines
|
||||
|
||||
The following are some style guidelines that are specific to the Secure Team.
|
||||
|
|
|
@ -175,9 +175,9 @@ Jobs can have an `urgency` attribute set, which can be `:high`,
|
|||
|
||||
| **Urgency** | **Queue Scheduling Target** | **Execution Latency Requirement** |
|
||||
|--------------|-----------------------------|------------------------------------|
|
||||
| `:high` | 100 milliseconds | p50 of 1 second, p99 of 10 seconds |
|
||||
| `:low` | 1 minute | Maximum run time of 1 hour |
|
||||
| `:throttled` | None | Maximum run time of 1 hour |
|
||||
| `:high` | 10 seconds | p50 of 1 second, p99 of 10 seconds |
|
||||
| `:low` | 1 minute | Maximum run time of 5 minutes |
|
||||
| `:throttled` | None | Maximum run time of 5 minutes |
|
||||
|
||||
To set a job's urgency, use the `urgency` class method:
|
||||
|
||||
|
|
|
@ -665,6 +665,7 @@ appear to be associated to any of the services running, since they all appear to
|
|||
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | | Triggers enabled |
|
||||
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
|
||||
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
|
||||
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
|
||||
| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
|
||||
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
|
||||
| `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node |
|
||||
|
|
|
@ -54,12 +54,14 @@ class Feature
|
|||
# unless set explicitly. The default is `disabled`
|
||||
# TODO: remove the `default_enabled:` and read it from the `defintion_yaml`
|
||||
# check: https://gitlab.com/gitlab-org/gitlab/-/issues/30228
|
||||
def enabled?(key, thing = nil, default_enabled: false)
|
||||
def enabled?(key, thing = nil, type: :development, default_enabled: false)
|
||||
if check_feature_flags_definition?
|
||||
if thing && !thing.respond_to?(:flipper_id)
|
||||
raise InvalidFeatureFlagError,
|
||||
"The thing '#{thing.class.name}' for feature flag '#{key}' needs to include `FeatureGate` or implement `flipper_id`"
|
||||
end
|
||||
|
||||
Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
|
||||
end
|
||||
|
||||
# During setup the database does not exist yet. So we haven't stored a value
|
||||
|
@ -75,9 +77,9 @@ class Feature
|
|||
!default_enabled || Feature.persisted_name?(feature.name) ? feature.enabled?(thing) : true
|
||||
end
|
||||
|
||||
def disabled?(key, thing = nil, default_enabled: false)
|
||||
def disabled?(key, thing = nil, type: :development, default_enabled: false)
|
||||
# we need to make different method calls to make it easy to mock / define expectations in test mode
|
||||
thing.nil? ? !enabled?(key, default_enabled: default_enabled) : !enabled?(key, thing, default_enabled: default_enabled)
|
||||
thing.nil? ? !enabled?(key, type: type, default_enabled: default_enabled) : !enabled?(key, thing, type: type, default_enabled: default_enabled)
|
||||
end
|
||||
|
||||
def enable(key, thing = true)
|
||||
|
@ -129,6 +131,12 @@ class Feature
|
|||
def register_feature_groups
|
||||
end
|
||||
|
||||
def register_definitions
|
||||
return unless check_feature_flags_definition?
|
||||
|
||||
Feature::Definition.load_all!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def flipper
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Feature
|
||||
class Definition
|
||||
include ::Feature::Shared
|
||||
|
||||
attr_reader :path
|
||||
attr_reader :attributes
|
||||
|
||||
PARAMS.each do |param|
|
||||
define_method(param) do
|
||||
attributes[param]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(path, opts = {})
|
||||
@path = path
|
||||
@attributes = {}
|
||||
|
||||
# assign nil, for all unknown opts
|
||||
PARAMS.each do |param|
|
||||
@attributes[param] = opts[param]
|
||||
end
|
||||
end
|
||||
|
||||
def key
|
||||
name.to_sym
|
||||
end
|
||||
|
||||
def validate!
|
||||
unless name.present?
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag is missing name"
|
||||
end
|
||||
|
||||
unless path.present?
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing path"
|
||||
end
|
||||
|
||||
unless type.present?
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing type. Ensure to update #{path}"
|
||||
end
|
||||
|
||||
unless Definition::TYPES.include?(type.to_sym)
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' type '#{type}' is invalid. Ensure to update #{path}"
|
||||
end
|
||||
|
||||
unless File.basename(path, ".yml") == name
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid path: '#{path}'. Ensure to update #{path}"
|
||||
end
|
||||
|
||||
unless File.basename(File.dirname(path)) == type
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' has an invalid type: '#{path}'. Ensure to update #{path}"
|
||||
end
|
||||
|
||||
if default_enabled.nil?
|
||||
raise Feature::InvalidFeatureFlagError, "Feature flag '#{name}' is missing default_enabled. Ensure to update #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def valid_usage!(type_in_code:, default_enabled_in_code:)
|
||||
unless Array(type).include?(type_in_code.to_s)
|
||||
# Raise exception in test and dev
|
||||
raise Feature::InvalidFeatureFlagError, "The `type:` of `#{key}` is not equal to config: " \
|
||||
"#{type_in_code} vs #{type}. Ensure to use valid type in #{path} or ensure that you use " \
|
||||
"a valid syntax: #{TYPES.dig(type, :example)}"
|
||||
end
|
||||
|
||||
# We accept an array of defaults as some features are undefined
|
||||
# and have `default_enabled: true/false`
|
||||
unless Array(default_enabled).include?(default_enabled_in_code)
|
||||
# Raise exception in test and dev
|
||||
raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \
|
||||
"#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def to_h
|
||||
attributes
|
||||
end
|
||||
|
||||
class << self
|
||||
def paths
|
||||
@paths ||= [Rails.root.join('config', 'feature_flags', '**', '*.yml')]
|
||||
end
|
||||
|
||||
def definitions
|
||||
@definitions ||= {}
|
||||
end
|
||||
|
||||
def load_all!
|
||||
definitions.clear
|
||||
|
||||
paths.each do |glob_path|
|
||||
load_all_from_path!(glob_path)
|
||||
end
|
||||
|
||||
definitions
|
||||
end
|
||||
|
||||
def valid_usage!(key, type:, default_enabled:)
|
||||
if definition = definitions[key.to_sym]
|
||||
definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled)
|
||||
elsif type_definition = self::TYPES[type]
|
||||
raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional]
|
||||
else
|
||||
raise InvalidFeatureFlagError, "Unknown feature flag type used: `#{type}`"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_from_file(path)
|
||||
definition = File.read(path)
|
||||
definition = YAML.safe_load(definition)
|
||||
definition.deep_symbolize_keys!
|
||||
|
||||
self.new(path, definition).tap(&:validate!)
|
||||
rescue => e
|
||||
raise Feature::InvalidFeatureFlagError, "Invalid definition for `#{path}`: #{e.message}"
|
||||
end
|
||||
|
||||
def load_all_from_path!(glob_path)
|
||||
Dir.glob(glob_path).each do |path|
|
||||
definition = load_from_file(path)
|
||||
|
||||
if previous = definitions[definition.key]
|
||||
raise InvalidFeatureFlagError, "Feature flag '#{definition.key}' is already defined in '#{previous.path}'"
|
||||
end
|
||||
|
||||
definitions[definition.key] = definition
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Feature::Definition.prepend_if_ee('EE::Feature::Definition')
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This file can contain only simple constructs as it is shared between:
|
||||
# 1. `Pure Ruby`: `bin/feature-flag`
|
||||
# 2. `GitLab Rails`: `lib/feature/definition.rb`
|
||||
|
||||
class Feature
|
||||
module Shared
|
||||
# optional: defines if a on-disk definition is required for this feature flag type
|
||||
# rollout_issue: defines if `bin/feature-flag` asks for rollout issue
|
||||
# example: usage being shown when exception is raised
|
||||
TYPES = {
|
||||
development: {
|
||||
description: 'Short lived, used to enable unfinished code to be deployed',
|
||||
optional: true,
|
||||
rollout_issue: true,
|
||||
example: <<-EOS
|
||||
Feature.enabled?(:my_feature_flag)
|
||||
Feature.enabled?(:my_feature_flag, type: :development)
|
||||
EOS
|
||||
}
|
||||
}.freeze
|
||||
|
||||
PARAMS = %i[
|
||||
name
|
||||
default_enabled
|
||||
type
|
||||
introduced_by_url
|
||||
rollout_issue_url
|
||||
group
|
||||
].freeze
|
||||
end
|
||||
end
|
|
@ -42,6 +42,8 @@ module Gitlab
|
|||
snippet.repository.expire_exists_cache
|
||||
|
||||
raise SnippetRepositoryError, _("Invalid repository bundle for snippet with id %{snippet_id}") % { snippet_id: snippet.id }
|
||||
else
|
||||
Snippets::UpdateStatisticsService.new(snippet).execute
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -159,8 +159,7 @@ module Gitlab
|
|||
usage_counters,
|
||||
user_preferences_usage,
|
||||
ingress_modsecurity_usage,
|
||||
container_expiration_policies_usage,
|
||||
merge_requests_usage(last_28_days_time_period)
|
||||
container_expiration_policies_usage
|
||||
).tap do |data|
|
||||
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
|
||||
end
|
||||
|
@ -405,23 +404,19 @@ module Gitlab
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def merge_requests_usage(time_period)
|
||||
def merge_requests_users(time_period)
|
||||
query =
|
||||
Event
|
||||
.where(target_type: Event::TARGET_TYPES[:merge_request].to_s)
|
||||
.where(time_period)
|
||||
|
||||
merge_request_users = distinct_count(
|
||||
distinct_count(
|
||||
query,
|
||||
:author_id,
|
||||
batch_size: 5_000, # Based on query performance, this is the optimal batch size.
|
||||
start: User.minimum(:id),
|
||||
finish: User.maximum(:id)
|
||||
)
|
||||
|
||||
{
|
||||
merge_requests_users: merge_request_users
|
||||
}
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
@ -477,9 +472,10 @@ module Gitlab
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Omitted because no user, creator or author associated: `lfs_objects`, `pool_repositories`, `web_hooks`
|
||||
def usage_activity_by_stage_create(time_period)
|
||||
{}
|
||||
{}.tap do |h|
|
||||
h[:merge_requests_users] = merge_requests_users(time_period) if time_period.present?
|
||||
end
|
||||
end
|
||||
|
||||
# Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`
|
||||
|
|
|
@ -19552,6 +19552,9 @@ msgstr ""
|
|||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
msgid "Revoked"
|
||||
msgstr ""
|
||||
|
||||
msgid "Revoked impersonation token %{token_name}!"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
load File.expand_path('../../bin/feature-flag', __dir__)
|
||||
|
||||
RSpec.describe 'bin/feature-flag' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
describe FeatureFlagCreator do
|
||||
let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url] }
|
||||
let(:options) { FeatureFlagOptionParser.parse(argv) }
|
||||
let(:creator) { described_class.new(options) }
|
||||
let(:existing_flag) { File.join('config', 'feature_flags', 'development', 'existing-feature-flag.yml') }
|
||||
|
||||
before do
|
||||
# create a dummy feature flag
|
||||
FileUtils.mkdir_p(File.dirname(existing_flag))
|
||||
File.write(existing_flag, '{}')
|
||||
|
||||
# ignore writes
|
||||
allow(File).to receive(:write).and_return(true)
|
||||
|
||||
# ignore stdin
|
||||
allow($stdin).to receive(:gets).and_raise('EOF')
|
||||
|
||||
# ignore Git commands
|
||||
allow(creator).to receive(:branch_name) { 'feature-branch' }
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_f(existing_flag)
|
||||
end
|
||||
|
||||
subject { creator.execute }
|
||||
|
||||
it 'properly creates a feature flag' do
|
||||
expect(File).to receive(:write).with(
|
||||
File.join('config', 'feature_flags', 'development', 'feature-flag-name.yml'),
|
||||
anything)
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to output(/name: feature-flag-name/).to_stdout
|
||||
end
|
||||
|
||||
context 'when running on master' do
|
||||
it 'requires feature branch' do
|
||||
expect(creator).to receive(:branch_name) { 'master' }
|
||||
|
||||
expect { subject }.to raise_error(FeatureFlagHelpers::Abort, /Create a branch first/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'validates feature flag name' do
|
||||
where(:argv, :ex) do
|
||||
%w[.invalid.feature.flag] | /Provide a name for the feature flag that is/
|
||||
%w[existing-feature-flag] | /already exists!/
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
expect { subject }.to raise_error(ex)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe FeatureFlagOptionParser do
|
||||
describe '.parse' do
|
||||
where(:param, :argv, :result) do
|
||||
:name | %w[foo] | 'foo'
|
||||
:amend | %w[foo --amend] | true
|
||||
:force | %w[foo -f] | true
|
||||
:force | %w[foo --force] | true
|
||||
:ee | %w[foo -e] | true
|
||||
:ee | %w[foo --ee] | true
|
||||
:introduced_by_url | %w[foo -m https://url] | 'https://url'
|
||||
:introduced_by_url | %w[foo --introduced-by-url https://url] | 'https://url'
|
||||
:rollout_issue_url | %w[foo -i https://url] | 'https://url'
|
||||
:rollout_issue_url | %w[foo --rollout-issue-url https://url] | 'https://url'
|
||||
:dry_run | %w[foo -n] | true
|
||||
:dry_run | %w[foo --dry-run] | true
|
||||
:type | %w[foo -t development] | :development
|
||||
:type | %w[foo --type development] | :development
|
||||
:type | %w[foo -t invalid] | nil
|
||||
:type | %w[foo --type invalid] | nil
|
||||
:group | %w[foo -g group::memory] | 'group::memory'
|
||||
:group | %w[foo --group group::memory] | 'group::memory'
|
||||
:group | %w[foo -g invalid] | nil
|
||||
:group | %w[foo --group invalid] | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
options = described_class.parse(Array(argv))
|
||||
|
||||
expect(options.public_send(param)).to eq(result)
|
||||
end
|
||||
end
|
||||
|
||||
it 'missing feature flag name' do
|
||||
expect do
|
||||
expect { described_class.parse(%w[--amend]) }.to output(/Feature flag name is required/).to_stdout
|
||||
end.to raise_error(FeatureFlagHelpers::Abort)
|
||||
end
|
||||
|
||||
it 'parses -h' do
|
||||
expect do
|
||||
expect { described_class.parse(%w[foo -h]) }.to output(/Usage:/).to_stdout
|
||||
end.to raise_error(FeatureFlagHelpers::Done)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.read_type' do
|
||||
let(:type) { 'development' }
|
||||
|
||||
it 'reads type from $stdin' do
|
||||
expect($stdin).to receive(:gets).and_return(type)
|
||||
expect do
|
||||
expect(described_class.read_type).to eq(:development)
|
||||
end.to output(/specify the type/).to_stdout
|
||||
end
|
||||
|
||||
context 'invalid type given' do
|
||||
let(:type) { 'invalid' }
|
||||
|
||||
it 'shows error message and retries' do
|
||||
expect($stdin).to receive(:gets).and_return(type)
|
||||
expect($stdin).to receive(:gets).and_raise('EOF')
|
||||
|
||||
expect do
|
||||
expect { described_class.read_type }.to raise_error(/EOF/)
|
||||
end.to output(/specify the type/).to_stdout
|
||||
.and output(/Invalid type specified/).to_stderr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.read_group' do
|
||||
let(:group) { 'group::memory' }
|
||||
|
||||
it 'reads type from $stdin' do
|
||||
expect($stdin).to receive(:gets).and_return(group)
|
||||
expect do
|
||||
expect(described_class.read_group).to eq('group::memory')
|
||||
end.to output(/specify the group/).to_stdout
|
||||
end
|
||||
|
||||
context 'invalid group given' do
|
||||
let(:type) { 'invalid' }
|
||||
|
||||
it 'shows error message and retries' do
|
||||
expect($stdin).to receive(:gets).and_return(type)
|
||||
expect($stdin).to receive(:gets).and_raise('EOF')
|
||||
|
||||
expect do
|
||||
expect { described_class.read_group }.to raise_error(/EOF/)
|
||||
end.to output(/specify the group/).to_stdout
|
||||
.and output(/Group needs to include/).to_stderr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.rollout_issue_url' do
|
||||
let(:options) { OpenStruct.new(name: 'foo', type: :development) }
|
||||
let(:url) { 'https://issue' }
|
||||
|
||||
it 'reads type from $stdin' do
|
||||
expect($stdin).to receive(:gets).and_return(url)
|
||||
expect do
|
||||
expect(described_class.read_issue_url(options)).to eq('https://issue')
|
||||
end.to output(/Paste URL here/).to_stdout
|
||||
end
|
||||
|
||||
context 'invalid URL given' do
|
||||
let(:type) { 'invalid' }
|
||||
|
||||
it 'shows error message and retries' do
|
||||
expect($stdin).to receive(:gets).and_return(type)
|
||||
expect($stdin).to receive(:gets).and_raise('EOF')
|
||||
|
||||
expect do
|
||||
expect { described_class.read_issue_url(options) }.to raise_error(/EOF/)
|
||||
end.to output(/Paste URL here/).to_stdout
|
||||
.and output(/URL needs to start/).to_stderr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -63,11 +63,17 @@ describe('Monitoring store actions', () => {
|
|||
let store;
|
||||
let state;
|
||||
|
||||
let dispatch;
|
||||
let commit;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore({ getters });
|
||||
state = store.state.monitoringDashboard;
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
commit = jest.fn();
|
||||
dispatch = jest.fn();
|
||||
|
||||
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
|
||||
const q = new Promise((resolve, reject) => {
|
||||
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
|
||||
|
@ -200,12 +206,8 @@ describe('Monitoring store actions', () => {
|
|||
// Metrics dashboard
|
||||
|
||||
describe('fetchDashboard', () => {
|
||||
let dispatch;
|
||||
let commit;
|
||||
const response = metricsDashboardResponse;
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn();
|
||||
commit = jest.fn();
|
||||
state.dashboardEndpoint = '/dashboard';
|
||||
});
|
||||
|
||||
|
@ -292,14 +294,6 @@ describe('Monitoring store actions', () => {
|
|||
});
|
||||
|
||||
describe('receiveMetricsDashboardSuccess', () => {
|
||||
let commit;
|
||||
let dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
commit = jest.fn();
|
||||
dispatch = jest.fn();
|
||||
});
|
||||
|
||||
it('stores groups', () => {
|
||||
const response = metricsDashboardResponse;
|
||||
receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
|
||||
|
@ -359,13 +353,8 @@ describe('Monitoring store actions', () => {
|
|||
// Metrics
|
||||
|
||||
describe('fetchDashboardData', () => {
|
||||
let commit;
|
||||
let dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
commit = jest.fn();
|
||||
dispatch = jest.fn();
|
||||
|
||||
state.timeRange = defaultTimeRange;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Feature::Definition do
|
||||
let(:attributes) do
|
||||
{ name: 'feature_flag',
|
||||
type: 'development',
|
||||
default_enabled: true }
|
||||
end
|
||||
|
||||
let(:path) { File.join('development', 'feature_flag.yml') }
|
||||
let(:definition) { described_class.new(path, attributes) }
|
||||
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
|
||||
|
||||
describe '#key' do
|
||||
subject { definition.key }
|
||||
|
||||
it 'returns a symbol from name' do
|
||||
is_expected.to eq(:feature_flag)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate!' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:param, :value, :result) do
|
||||
:name | nil | /Feature flag is missing name/
|
||||
:path | nil | /Feature flag 'feature_flag' is missing path/
|
||||
:type | nil | /Feature flag 'feature_flag' is missing type/
|
||||
:type | 'invalid' | /Feature flag 'feature_flag' type 'invalid' is invalid/
|
||||
:path | 'development/invalid.yml' | /Feature flag 'feature_flag' has an invalid path/
|
||||
:path | 'invalid/feature_flag.yml' | /Feature flag 'feature_flag' has an invalid type/
|
||||
:default_enabled | nil | /Feature flag 'feature_flag' is missing default_enabled/
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:params) { attributes.merge(path: path) }
|
||||
|
||||
before do
|
||||
params[param] = value
|
||||
end
|
||||
|
||||
it do
|
||||
expect do
|
||||
described_class.new(
|
||||
params[:path], params.except(:path)
|
||||
).validate!
|
||||
end.to raise_error(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid_usage!' do
|
||||
context 'validates type' do
|
||||
it 'raises exception for invalid type' do
|
||||
expect { definition.valid_usage!(type_in_code: :invalid, default_enabled_in_code: false) }
|
||||
.to raise_error(/The `type:` of `feature_flag` is not equal to config/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'validates default enabled' do
|
||||
it 'raises exception for different value' do
|
||||
expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) }
|
||||
.to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.paths' do
|
||||
it 'returns at least one path' do
|
||||
expect(described_class.paths).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '.load_from_file' do
|
||||
it 'properly loads a definition from file' do
|
||||
expect(File).to receive(:read).with(path) { yaml_content }
|
||||
|
||||
expect(described_class.send(:load_from_file, path).attributes)
|
||||
.to eq(definition.attributes)
|
||||
end
|
||||
|
||||
context 'for missing file' do
|
||||
let(:path) { 'missing/feature-flag/file.yml' }
|
||||
|
||||
it 'raises exception' do
|
||||
expect do
|
||||
described_class.send(:load_from_file, path)
|
||||
end.to raise_error(/Invalid definition for/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for invalid definition' do
|
||||
it 'raises exception' do
|
||||
expect(File).to receive(:read).with(path) { '{}' }
|
||||
|
||||
expect do
|
||||
described_class.send(:load_from_file, path)
|
||||
end.to raise_error(/Feature flag is missing name/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.load_all!' do
|
||||
let(:store1) { Dir.mktmpdir('path1') }
|
||||
let(:store2) { Dir.mktmpdir('path2') }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:paths).and_return(
|
||||
[
|
||||
File.join(store1, '**', '*.yml'),
|
||||
File.join(store2, '**', '*.yml')
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it "when there's no feature flags a list of definitions is empty" do
|
||||
expect(described_class.load_all!).to be_empty
|
||||
end
|
||||
|
||||
it "when there's a single feature flag it properly loads them" do
|
||||
write_feature_flag(store1, path, yaml_content)
|
||||
|
||||
expect(described_class.load_all!).to be_one
|
||||
end
|
||||
|
||||
it "when the same feature flag is stored multiple times raises exception" do
|
||||
write_feature_flag(store1, path, yaml_content)
|
||||
write_feature_flag(store2, path, yaml_content)
|
||||
|
||||
expect { described_class.load_all! }
|
||||
.to raise_error(/Feature flag 'feature_flag' is already defined/)
|
||||
end
|
||||
|
||||
it "when one of the YAMLs is invalid it does raise exception" do
|
||||
write_feature_flag(store1, path, '{}')
|
||||
|
||||
expect { described_class.load_all! }
|
||||
.to raise_error(/Feature flag is missing name/)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(store1)
|
||||
FileUtils.rm_rf(store2)
|
||||
end
|
||||
|
||||
def write_feature_flag(store, path, content)
|
||||
path = File.join(store, path)
|
||||
dir = File.dirname(path)
|
||||
FileUtils.mkdir_p(dir)
|
||||
File.write(path, content)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_usage!' do
|
||||
before do
|
||||
allow(described_class).to receive(:definitions) do
|
||||
{ definition.key => definition }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a known feature flag is used' do
|
||||
it 'validates it usage' do
|
||||
expect(definition).to receive(:valid_usage!)
|
||||
|
||||
described_class.valid_usage!(:feature_flag, type: :development, default_enabled: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an unknown feature flag is used' do
|
||||
context 'for a type that is required to have all feature flags registered' do
|
||||
before do
|
||||
stub_const('Feature::Shared::TYPES', {
|
||||
development: { optional: false }
|
||||
})
|
||||
end
|
||||
|
||||
it 'raises exception' do
|
||||
expect do
|
||||
described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false)
|
||||
end.to raise_error(/Missing feature definition for `unknown_feature_flag`/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a type that is optional' do
|
||||
before do
|
||||
stub_const('Feature::Shared::TYPES', {
|
||||
development: { optional: true }
|
||||
})
|
||||
end
|
||||
|
||||
it 'does not raise exception' do
|
||||
expect do
|
||||
described_class.valid_usage!(:unknown_feature_flag, type: :development, default_enabled: false)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an unknown type' do
|
||||
it 'raises exception' do
|
||||
expect do
|
||||
described_class.valid_usage!(:unknown_feature_flag, type: :unknown_type, default_enabled: false)
|
||||
end.to raise_error(/Unknown feature flag type used: `unknown_type`/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -242,6 +242,36 @@ RSpec.describe Feature, stub_feature_flags: false do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'validates usage of feature flag with YAML definition' do
|
||||
let(:definition) do
|
||||
Feature::Definition.new('development/my_feature_flag.yml',
|
||||
name: 'my_feature_flag',
|
||||
type: 'development',
|
||||
default_enabled: false
|
||||
).tap(&:validate!)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Feature::Definition).to receive(:definitions) do
|
||||
{ definition.key => definition }
|
||||
end
|
||||
end
|
||||
|
||||
it 'when usage is correct' do
|
||||
expect { described_class.enabled?(:my_feature_flag) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'when invalid type is used' do
|
||||
expect { described_class.enabled?(:my_feature_flag, type: :licensed) }
|
||||
.to raise_error(/The `type:` of/)
|
||||
end
|
||||
|
||||
it 'when invalid default_enabled is used' do
|
||||
expect { described_class.enabled?(:my_feature_flag, default_enabled: true) }
|
||||
.to raise_error(/The `default_enabled:` of/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.disable?' do
|
||||
|
|
|
@ -35,6 +35,12 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
|
|||
end
|
||||
end
|
||||
|
||||
it 'does not call snippet update statistics service' do
|
||||
expect(Snippets::UpdateStatisticsService).not_to receive(:new).with(snippet)
|
||||
|
||||
restorer.restore
|
||||
end
|
||||
|
||||
context 'when the repository creation fails' do
|
||||
it 'returns false' do
|
||||
allow_any_instance_of(Gitlab::BackgroundMigration::BackfillSnippetRepositories).to receive(:perform_by_ids).and_return(nil)
|
||||
|
@ -66,6 +72,10 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
|
|||
|
||||
before do
|
||||
expect(exporter.save).to be_truthy
|
||||
|
||||
allow_next_instance_of(Snippets::RepositoryValidationService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is valid' do
|
||||
|
@ -115,5 +125,19 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'refreshes snippet statistics' do
|
||||
expect(snippet.statistics.commit_count).to be_zero
|
||||
expect(snippet.statistics.file_count).to be_zero
|
||||
expect(snippet.statistics.repository_size).to be_zero
|
||||
|
||||
expect(Snippets::UpdateStatisticsService).to receive(:new).with(snippet).and_call_original
|
||||
|
||||
restorer.restore
|
||||
|
||||
expect(snippet.statistics.commit_count).not_to be_zero
|
||||
expect(snippet.statistics.file_count).not_to be_zero
|
||||
expect(snippet.statistics.repository_size).not_to be_zero
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,22 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
context 'for create' do
|
||||
it 'include usage_activity_by_stage data' do
|
||||
expect(described_class.uncached_data[:usage_activity_by_stage][:create])
|
||||
.not_to include(
|
||||
:merge_requests_users
|
||||
)
|
||||
end
|
||||
|
||||
it 'includes monthly usage_activity_by_stage data' do
|
||||
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:create])
|
||||
.to include(
|
||||
:merge_requests_users
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'ensures recorded_at is set before any other usage data calculation' do
|
||||
%i(alt_usage_data redis_usage_data distinct_count count).each do |method|
|
||||
expect(described_class).not_to receive(method)
|
||||
|
@ -662,7 +678,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.merge_requests_usage' do
|
||||
describe '.merge_requests_users' do
|
||||
let(:time_period) { { created_at: 2.days.ago..Time.current } }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:other_user) { create(:user) }
|
||||
|
@ -679,9 +695,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
end
|
||||
|
||||
it 'returns the distinct count of users using merge requests (via events table) within the specified time period' do
|
||||
expect(described_class.merge_requests_usage(time_period)).to eq(
|
||||
merge_requests_users: 2
|
||||
)
|
||||
expect(described_class.merge_requests_users(time_period)).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -155,6 +155,9 @@ RSpec.configure do |config|
|
|||
config.before(:suite) do
|
||||
Timecop.safe_mode = true
|
||||
TestEnv.init
|
||||
|
||||
# Reload all feature flags definitions
|
||||
Feature.register_definitions
|
||||
end
|
||||
|
||||
config.after(:all) do
|
||||
|
|
|
@ -78,7 +78,6 @@ module UsageDataHelpers
|
|||
labels
|
||||
lfs_objects
|
||||
merge_requests
|
||||
merge_requests_users
|
||||
milestone_lists
|
||||
milestones
|
||||
notes
|
||||
|
|
Loading…
Reference in New Issue