Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/test-metadata.gitlab-ci.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue13
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue25
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/constants.js11
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/devops_adoption.js20
-rw-r--r--app/assets/javascripts/admin/dev_ops_report/devops_score_empty_state.js27
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue22
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js30
-rw-r--r--app/assets/stylesheets/framework/editor-lite.scss18
-rw-r--r--app/assets/stylesheets/framework/spinner.scss10
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/models/broadcast_message.rb1
-rw-r--r--app/models/concerns/issue_available_features.rb17
-rw-r--r--app/models/concerns/triggerable_hooks.rb3
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/operations/feature_flag.rb17
-rw-r--r--app/services/feature_flags/update_service.rb4
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml2
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/shared/web_hooks/_form.html.haml6
-rw-r--r--changelogs/unreleased/272986-fj-disallow-webide-route-in-robots.yml5
-rw-r--r--changelogs/unreleased/rmay-216344.yml5
-rw-r--r--changelogs/unreleased/sk-220898-feature-flag-webhook.yml5
-rw-r--r--db/migrate/20200908212414_add_feature_flag_events_to_web_hooks.rb9
-rw-r--r--db/migrate/20201012073022_remove_not_null_constraint_on_framework.rb33
-rw-r--r--db/schema_migrations/202009082124141
-rw-r--r--db/schema_migrations/202010120730221
-rw-r--r--db/structure.sql5
-rw-r--r--doc/api/README.md6
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql33
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json85
-rw-r--r--doc/api/graphql/reference/index.md18
-rw-r--r--doc/api/group_level_variables.md6
-rw-r--r--doc/api/groups.md6
-rw-r--r--doc/api/import.md6
-rw-r--r--doc/api/instance_clusters.md6
-rw-r--r--doc/api/issue_links.md6
-rw-r--r--doc/api/issues_statistics.md6
-rw-r--r--doc/api/job_artifacts.md6
-rw-r--r--doc/api/license.md6
-rw-r--r--doc/api/managed_licenses.md6
-rw-r--r--doc/api/members.md6
-rw-r--r--doc/api/namespaces.md6
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/notification_settings.md6
-rw-r--r--doc/api/personal_access_tokens.md6
-rw-r--r--doc/api/resource_label_events.md6
-rw-r--r--doc/api/scim.md6
-rw-r--r--doc/api/services.md6
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/api/sidekiq_metrics.md6
-rw-r--r--doc/api/statistics.md6
-rw-r--r--doc/api/system_hooks.md6
-rw-r--r--doc/api/templates/dockerfiles.md3
-rw-r--r--doc/api/templates/gitignores.md3
-rw-r--r--doc/api/templates/licenses.md3
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/api/v3_to_v4.md6
-rw-r--r--doc/api/version.md6
-rw-r--r--doc/development/feature_flags/development.md24
-rw-r--r--doc/user/project/integrations/webhooks.md49
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/admin/ci/variables.rb2
-rw-r--r--lib/api/admin/instance_clusters.rb2
-rw-r--r--lib/api/admin/sidekiq.rb2
-rw-r--r--lib/api/appearance.rb2
-rw-r--r--lib/api/applications.rb2
-rw-r--r--lib/api/avatar.rb2
-rw-r--r--lib/api/award_emoji.rb14
-rw-r--r--lib/gitlab/data_builder/feature_flag.rb19
-rw-r--r--locale/gitlab.pot15
-rw-r--r--public/robots.txt1
-rwxr-xr-xscripts/generate-test-mapping19
-rwxr-xr-xscripts/pack-test-mapping19
-rw-r--r--scripts/rspec_helpers.sh37
-rwxr-xr-xscripts/unpack-test-mapping17
-rw-r--r--spec/crystalball_env.rb25
-rw-r--r--spec/factories/project_hooks.rb1
-rw-r--r--spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js21
-rw-r--r--spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js52
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js10
-rw-r--r--spec/helpers/groups_helper_spec.rb20
-rw-r--r--spec/lib/api/every_api_endpoint_spec.rb8
-rw-r--r--spec/lib/gitlab/data_builder/feature_flag_spec.rb25
-rw-r--r--spec/models/broadcast_message_spec.rb6
-rw-r--r--spec/models/operations/feature_flag_spec.rb34
-rw-r--r--spec/requests/robots_txt_spec.rb1
-rw-r--r--spec/services/feature_flags/update_service_spec.rb7
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb42
-rw-r--r--spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb16
-rw-r--r--spec/tooling/lib/tooling/test_map_generator_spec.rb109
-rw-r--r--spec/tooling/lib/tooling/test_map_packer_spec.rb77
-rw-r--r--tooling/lib/tooling/crystalball/coverage_lines_execution_detector.rb44
-rw-r--r--tooling/lib/tooling/crystalball/coverage_lines_strategy.rb23
-rw-r--r--tooling/lib/tooling/test_map_generator.rb36
-rw-r--r--tooling/lib/tooling/test_map_packer.rb58
103 files changed, 1329 insertions, 125 deletions
diff --git a/.gitignore b/.gitignore
index 25c42cdb56d..8a47cc8d20b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,7 @@ eslint-report.html
/.gitlab_pages_secret
/.gitlab_kas_secret
/webpack-report/
+/crystalball/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d56ff9bab7a..2156ca19c73 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -59,6 +59,8 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
+ RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json
+ RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json
BUILD_ASSETS_IMAGE: "false"
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index f44ab9deb08..5a8f2651b6f 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -20,6 +20,7 @@
variables:
RUBY_GC_MALLOC_LIMIT: 67108864
RUBY_GC_MALLOC_LIMIT_MAX: 134217728
+ CRYSTALBALL: "true"
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"]
script:
- *base-script
@@ -29,6 +30,7 @@
when: always
paths:
- coverage/
+ - crystalball/
- knapsack/
- rspec_flaky/
- rspec_profiling/
diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml
index 08c793120ab..ba5b3f98689 100644
--- a/.gitlab/ci/test-metadata.gitlab-ci.yml
+++ b/.gitlab/ci/test-metadata.gitlab-ci.yml
@@ -9,6 +9,7 @@
- knapsack/
- rspec_flaky/
- rspec_profiling/
+ - crystalball/
retrieve-tests-metadata:
extends:
@@ -41,3 +42,4 @@ update-tests-metadata:
- run_timed_command "retry gem install bundler:1.17.3 fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- source ./scripts/rspec_helpers.sh
- update_tests_metadata
+ - update_tests_mapping
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 69664070920..eeeae75d501 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-bee8517ab043ff98c283a5f191e68e2bd75eb9de
+cf8e99ccc104f0a43f41e54896ee46a5e1b15a0a
diff --git a/Gemfile b/Gemfile
index d3671cac4f5..f845e9ccd77 100644
--- a/Gemfile
+++ b/Gemfile
@@ -386,6 +386,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'knapsack', '~> 1.17'
+ gem 'crystalball', '~> 0.7.0', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b7223af8b6..401286d900d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -199,6 +199,8 @@ GEM
safe_yaml (~> 1.0.0)
crass (1.0.6)
creole (0.5.0)
+ crystalball (0.7.0)
+ git
css_parser (1.7.0)
addressable
daemons (1.2.6)
@@ -1291,6 +1293,7 @@ DEPENDENCIES
connection_pool (~> 2.0)
countries (~> 3.0)
creole (~> 0.5.0)
+ crystalball (~> 0.7.0)
danger (~> 8.0.6)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.3.1)
diff --git a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue
new file mode 100644
index 00000000000..ee2fe00fe02
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_app.vue
@@ -0,0 +1,13 @@
+<script>
+import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
+
+export default {
+ name: 'DevopsAdoptionApp',
+ components: {
+ DevopsAdoptionEmptyState,
+ },
+};
+</script>
+<template>
+ <devops-adoption-empty-state />
+</template>
diff --git a/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue
new file mode 100644
index 00000000000..0fff9beb435
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/components/devops_adoption_empty_state.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { DEVOPS_ADOPTION_STRINGS } from '../constants';
+
+export default {
+ name: 'DevopsAdoptionEmptyState',
+ inject: ['emptyStateSvgPath'],
+ components: {
+ GlEmptyState,
+ GlButton,
+ },
+ i18n: DEVOPS_ADOPTION_STRINGS.emptyState,
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :description="$options.i18n.description"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button variant="info">{{ $options.i18n.button }}</gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/admin/dev_ops_report/constants.js b/app/assets/javascripts/admin/dev_ops_report/constants.js
new file mode 100644
index 00000000000..4f3b7879332
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/constants.js
@@ -0,0 +1,11 @@
+import { s__ } from '~/locale';
+
+export const DEVOPS_ADOPTION_STRINGS = {
+ emptyState: {
+ title: s__('DevopsAdoption|Add a segment to get started'),
+ description: s__(
+ 'DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team.',
+ ),
+ button: s__('DevopsAdoption|Add new segment'),
+ },
+};
diff --git a/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js b/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js
new file mode 100644
index 00000000000..45901a5634f
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/devops_adoption.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import DevopsAdoptionApp from './components/devops_adoption_app.vue';
+
+export default () => {
+ const el = document.querySelector('.js-devops-adoption');
+
+ if (!el) return false;
+
+ const { emptyStateSvgPath } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ emptyStateSvgPath,
+ },
+ render(h) {
+ return h(DevopsAdoptionApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/dev_ops_report/devops_score_empty_state.js b/app/assets/javascripts/admin/dev_ops_report/devops_score_empty_state.js
new file mode 100644
index 00000000000..0cb8d9be0e4
--- /dev/null
+++ b/app/assets/javascripts/admin/dev_ops_report/devops_score_empty_state.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import UserCallout from '~/user_callout';
+import UsagePingDisabled from './components/usage_ping_disabled.vue';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new UserCallout();
+
+ const emptyStateContainer = document.getElementById('js-devops-empty-state');
+
+ if (!emptyStateContainer) return false;
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
+
+ return new Vue({
+ el: emptyStateContainer,
+ provide: {
+ isAdmin: Boolean(isAdmin),
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+};
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index e1f9d858f2b..6f9b05c08ab 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -69,9 +69,12 @@ export default {
{
key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'),
- thClass: `gl-pointer-events-none gl-text-right gl-w-eighth`,
+ thClass: `gl-text-right gl-w-eighth`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
+ sortKey: 'SLA_DUE_AT',
+ sortable: true,
+ sortDirection: 'asc',
},
{
key: 'assignees',
@@ -253,13 +256,22 @@ export default {
this.redirecting = true;
},
fetchSortedData({ sortBy, sortDesc }) {
+ let sortKey;
+ // In bootstrap-vue v2.17.0, sortKey becomes natively supported and we can eliminate this function
+ const field = this.availableFields.find(({ key }) => key === sortBy);
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
- const sortingColumn = convertToSnakeCase(sortBy)
- .replace(/_.*/, '')
- .toUpperCase();
+
+ // Use `sortKey` if provided, otherwise fall back to existing algorithm
+ if (field?.sortKey) {
+ sortKey = field.sortKey;
+ } else {
+ sortKey = convertToSnakeCase(sortBy)
+ .replace(/_.*/, '')
+ .toUpperCase();
+ }
this.pagination = initialPaginationState;
- this.sort = `${sortingColumn}_${sortingDirection}`;
+ this.sort = `${sortKey}_${sortingDirection}`;
},
getSeverity(severity) {
return INCIDENT_SEVERITY[severity];
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index 643497003ba..325b74a414d 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,27 +1,5 @@
-import Vue from 'vue';
-import UserCallout from '~/user_callout';
-import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue';
+import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
+import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
-document.addEventListener('DOMContentLoaded', () => {
- // eslint-disable-next-line no-new
- new UserCallout();
-
- const emptyStateContainer = document.getElementById('js-devops-empty-state');
-
- if (!emptyStateContainer) return false;
-
- const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
-
- return new Vue({
- el: emptyStateContainer,
- provide: {
- isAdmin: Boolean(isAdmin),
- svgPath: emptyStateSvgPath,
- primaryButtonPath: enableUsagePingLink,
- docsLink,
- },
- render(h) {
- return h(UsagePingDisabled);
- },
- });
-});
+initDevOpsScoreEmptyState();
+initDevopAdoption();
diff --git a/app/assets/stylesheets/framework/editor-lite.scss b/app/assets/stylesheets/framework/editor-lite.scss
index 20fea7a82ca..c3b287a6c3d 100644
--- a/app/assets/stylesheets/framework/editor-lite.scss
+++ b/app/assets/stylesheets/framework/editor-lite.scss
@@ -1,3 +1,21 @@
+[data-editor-loading] {
+ @include gl-relative;
+ @include gl-display-flex;
+ @include gl-justify-content-center;
+ @include gl-align-items-center;
+
+ &::before {
+ content: '';
+ @include spinner(32px, 3px);
+ @include gl-absolute;
+ @include gl-z-index-1;
+ }
+
+ pre {
+ opacity: 0;
+ }
+}
+
[id^='editor-lite-'] {
height: 500px;
}
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index 581b7c37b5f..2aa0ab6c1eb 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -20,7 +20,7 @@
}
}
-.spinner {
+@mixin spinner($size: 16px, $border-width: 2px, $color: $orange-400) {
border-radius: 50%;
position: relative;
margin: 0 auto;
@@ -30,8 +30,12 @@
animation-iteration-count: infinite;
border-style: solid;
display: inline-flex;
- @include spinner-size(16px, 2px);
- @include spinner-color($orange-400);
+ @include spinner-size($size, $border-width);
+ @include spinner-color($color);
+}
+
+.spinner {
+ @include spinner;
&.spinner-md {
@include spinner-size(32px, 3px);
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 06a52457fd6..c9ad48fb1fc 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -174,6 +174,10 @@ module GroupsHelper
!multiple_members?(group)
end
+ def show_thanks_for_purchase_banner?
+ params.key?(:purchased_quantity) && params[:purchased_quantity].to_i > 0
+ end
+
private
def just_created?
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 856f86201ec..a8325e98095 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -103,6 +103,7 @@ class BroadcastMessage < ApplicationRecord
end
def matches_current_path(current_path)
+ return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank?
escaped = Regexp.escape(target_path).gsub('\\*', '.*')
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 6efb8103b7b..886db133a94 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -6,18 +6,25 @@
module IssueAvailableFeatures
extend ActiveSupport::Concern
- # EE only features are listed on EE::IssueAvailableFeatures
- def available_features_for_issue_types
- {}.with_indifferent_access
+ class_methods do
+ # EE only features are listed on EE::IssueAvailableFeatures
+ def available_features_for_issue_types
+ {}.with_indifferent_access
+ end
+ end
+
+ included do
+ scope :with_feature, ->(feature) { where(issue_type: available_features_for_issue_types[feature]) }
end
def issue_type_supports?(feature)
- unless available_features_for_issue_types.has_key?(feature)
+ unless self.class.available_features_for_issue_types.has_key?(feature)
raise ArgumentError, 'invalid feature'
end
- available_features_for_issue_types[feature].include?(issue_type)
+ self.class.available_features_for_issue_types[feature].include?(issue_type)
end
end
IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
+IssueAvailableFeatures::ClassMethods.prepend_if_ee('EE::IssueAvailableFeatures::ClassMethods')
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index b64a9e4f70b..e01da3b3f36 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -13,7 +13,8 @@ module TriggerableHooks
job_hooks: :job_events,
pipeline_hooks: :pipeline_events,
wiki_page_hooks: :wiki_page_events,
- deployment_hooks: :deployment_events
+ deployment_hooks: :deployment_events,
+ feature_flag_hooks: :feature_flag_events
}.freeze
extend ActiveSupport::Concern
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 2d1bdecc770..fa3578cda18 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -18,7 +18,8 @@ class ProjectHook < WebHook
:job_hooks,
:pipeline_hooks,
:wiki_page_hooks,
- :deployment_hooks
+ :deployment_hooks,
+ :feature_flag_hooks
]
belongs_to :project
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 104338b80d1..c9e52fe51f2 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -2,6 +2,7 @@
module Operations
class FeatureFlag < ApplicationRecord
+ include AfterCommitQueue
include AtomicInternalId
include IidRoutes
include Limitable
@@ -77,6 +78,22 @@ module Operations
Ability.issues_readable_by_user(issues, current_user)
end
+ def execute_hooks(current_user)
+ run_after_commit do
+ feature_flag_data = Gitlab::DataBuilder::FeatureFlag.build(self, current_user)
+ project.execute_hooks(feature_flag_data, :feature_flag_hooks)
+ end
+ end
+
+ def hook_attrs
+ {
+ id: id,
+ name: name,
+ description: description,
+ active: active
+ }
+ end
+
private
def version_associations
diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb
index c837e50b104..ed5e2e794b4 100644
--- a/app/services/feature_flags/update_service.rb
+++ b/app/services/feature_flags/update_service.rb
@@ -22,6 +22,10 @@ module FeatureFlags
audit_event = audit_event(feature_flag)
+ if feature_flag.active_changed?
+ feature_flag.execute_hooks(current_user)
+ end
+
if feature_flag.save
save_audit_event(audit_event)
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index c8209bf5099..4ffae7897a3 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -14,7 +14,7 @@
.tab-pane.active#devops_score_pane
= render 'report'
.tab-pane#devops_adoption_pane
- .js-devops-adoption
+ .js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } }
- else
= render 'report'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a1210bf2df4..9d5ec5008dc 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,6 +1,9 @@
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
+- if show_thanks_for_purchase_banner?
+ = render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
+
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 9c60201412c..e9ce443782a 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -78,6 +78,12 @@
%strong= s_('Webhooks|Deployment events')
%p.text-muted.ml-1
= s_('Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled')
+ %li
+ = form.check_box :feature_flag_events, class: 'form-check-input'
+ = form.label :feature_flag_events, class: 'list-label form-check-label ml-1' do
+ %strong= s_('Webhooks|Feature Flag events')
+ %p.text-muted.ml-1
+ = s_('Webhooks|This URL is triggered when a feature flag is turned on or off')
.form-group
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
.form-check
diff --git a/changelogs/unreleased/272986-fj-disallow-webide-route-in-robots.yml b/changelogs/unreleased/272986-fj-disallow-webide-route-in-robots.yml
new file mode 100644
index 00000000000..29f2cfb1111
--- /dev/null
+++ b/changelogs/unreleased/272986-fj-disallow-webide-route-in-robots.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow WebIDE route in robots.txt
+merge_request: 46117
+author:
+type: changed
diff --git a/changelogs/unreleased/rmay-216344.yml b/changelogs/unreleased/rmay-216344.yml
new file mode 100644
index 00000000000..d69abcf35e2
--- /dev/null
+++ b/changelogs/unreleased/rmay-216344.yml
@@ -0,0 +1,5 @@
+---
+title: Don't return target-specific broadcasts without a current path supplied
+merge_request: 46322
+author:
+type: fixed
diff --git a/changelogs/unreleased/sk-220898-feature-flag-webhook.yml b/changelogs/unreleased/sk-220898-feature-flag-webhook.yml
new file mode 100644
index 00000000000..0146fd60e2b
--- /dev/null
+++ b/changelogs/unreleased/sk-220898-feature-flag-webhook.yml
@@ -0,0 +1,5 @@
+---
+title: Add webhooks for feature flag
+merge_request: 41863
+author: Sashi
+type: added
diff --git a/db/migrate/20200908212414_add_feature_flag_events_to_web_hooks.rb b/db/migrate/20200908212414_add_feature_flag_events_to_web_hooks.rb
new file mode 100644
index 00000000000..40e2b37b390
--- /dev/null
+++ b/db/migrate/20200908212414_add_feature_flag_events_to_web_hooks.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddFeatureFlagEventsToWebHooks < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :web_hooks, :feature_flag_events, :boolean, null: false, default: false
+ end
+end
diff --git a/db/migrate/20201012073022_remove_not_null_constraint_on_framework.rb b/db/migrate/20201012073022_remove_not_null_constraint_on_framework.rb
new file mode 100644
index 00000000000..b8cc8984575
--- /dev/null
+++ b/db/migrate/20201012073022_remove_not_null_constraint_on_framework.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class RemoveNotNullConstraintOnFramework < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ GDPR_FRAMEWORK_ID = 1
+
+ disable_ddl_transaction!
+
+ class TmpComplianceProjectFrameworkSetting < ActiveRecord::Base
+ self.table_name = 'project_compliance_framework_settings'
+ self.primary_key = :project_id
+
+ include EachBatch
+ end
+
+ def up
+ change_column_null :project_compliance_framework_settings, :framework, true
+ end
+
+ def down
+ # Custom frameworks cannot be rolled back easily since we don't have enum for them.
+ # To make the database consistent, we mark them as GDPR framework.
+ # Note: framework customization will be implemented in the next 1-3 releases so data
+ # corruption due to the rollback is unlikely.
+ TmpComplianceProjectFrameworkSetting.each_batch(of: 100) do |query|
+ query.where(framework: nil).update_all(framework: GDPR_FRAMEWORK_ID)
+ end
+
+ change_column_null :project_compliance_framework_settings, :framework, false
+ end
+end
diff --git a/db/schema_migrations/20200908212414 b/db/schema_migrations/20200908212414
new file mode 100644
index 00000000000..208f9affc91
--- /dev/null
+++ b/db/schema_migrations/20200908212414
@@ -0,0 +1 @@
+a9605126178d887bbf526a4a33b7060b072eff7a8d6712e3552099f7e615f88b \ No newline at end of file
diff --git a/db/schema_migrations/20201012073022 b/db/schema_migrations/20201012073022
new file mode 100644
index 00000000000..b7ce136a7e3
--- /dev/null
+++ b/db/schema_migrations/20201012073022
@@ -0,0 +1 @@
+234711b96d3869fe826dfd71ae29e0f75e50302bc29a4e60f436ec76b4be3efb \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c117864ff81..7c4f5088069 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14837,7 +14837,7 @@ ALTER SEQUENCE project_ci_cd_settings_id_seq OWNED BY project_ci_cd_settings.id;
CREATE TABLE project_compliance_framework_settings (
project_id bigint NOT NULL,
- framework smallint NOT NULL,
+ framework smallint,
framework_id bigint,
CONSTRAINT check_d348de9e2d CHECK ((framework_id IS NOT NULL))
);
@@ -17295,7 +17295,8 @@ CREATE TABLE web_hooks (
encrypted_token_iv character varying,
encrypted_url character varying,
encrypted_url_iv character varying,
- deployment_events boolean DEFAULT false NOT NULL
+ deployment_events boolean DEFAULT false NOT NULL,
+ feature_flag_events boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE web_hooks_id_seq
diff --git a/doc/api/README.md b/doc/api/README.md
index c15687ce6ed..e077424da13 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# API Docs
Automate GitLab via a simple and powerful API.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index f5ed40ad802..83fd2e31e20 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2803,7 +2803,7 @@ type ComplianceFramework {
"""
Name of the compliance framework
"""
- name: ProjectSettingEnum!
+ name: String!
}
"""
@@ -6909,6 +6909,11 @@ type EpicIssue implements CurrentUserTodos & Noteable {
blocked: Boolean!
"""
+ Count of issues blocking this issue
+ """
+ blockedByCount: Int
+
+ """
Timestamp of when the issue was closed
"""
closedAt: Time
@@ -9177,6 +9182,11 @@ type Issue implements CurrentUserTodos & Noteable {
blocked: Boolean!
"""
+ Count of issues blocking this issue
+ """
+ blockedByCount: Int
+
+ """
Timestamp of when the issue was closed
"""
closedAt: Time
@@ -10207,6 +10217,16 @@ enum IssueSort {
SEVERITY_DESC
"""
+ Issues with earliest SLA due time shown first
+ """
+ SLA_DUE_AT_ASC
+
+ """
+ Issues with latest SLA due time shown first
+ """
+ SLA_DUE_AT_DESC
+
+ """
Updated at ascending order
"""
UPDATED_ASC
@@ -15701,17 +15721,6 @@ type ProjectPermissions {
uploadFile: Boolean!
}
-"""
-Names of compliance frameworks that can be assigned to a Project
-"""
-enum ProjectSettingEnum {
- gdpr
- hipaa
- pci_dss
- soc_2
- sox
-}
-
type ProjectStatistics {
"""
Build artifacts size of the project
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 58568bd7ea6..93c5968e64e 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -7545,8 +7545,8 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
- "kind": "ENUM",
- "name": "ProjectSettingEnum",
+ "kind": "SCALAR",
+ "name": "String",
"ofType": null
}
},
@@ -19042,6 +19042,20 @@
"deprecationReason": null
},
{
+ "name": "blockedByCount",
+ "description": "Count of issues blocking this issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "closedAt",
"description": "Timestamp of when the issue was closed",
"args": [
@@ -24984,6 +24998,20 @@
"deprecationReason": null
},
{
+ "name": "blockedByCount",
+ "description": "Count of issues blocking this issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "closedAt",
"description": "Timestamp of when the issue was closed",
"args": [
@@ -27868,6 +27896,18 @@
"description": "Published issues shown first",
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "SLA_DUE_AT_ASC",
+ "description": "Issues with earliest SLA due time shown first",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SLA_DUE_AT_DESC",
+ "description": "Issues with latest SLA due time shown first",
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"possibleTypes": null
@@ -45523,47 +45563,6 @@
"possibleTypes": null
},
{
- "kind": "ENUM",
- "name": "ProjectSettingEnum",
- "description": "Names of compliance frameworks that can be assigned to a Project",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- {
- "name": "gdpr",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "hipaa",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "pci_dss",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "soc_2",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "sox",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "possibleTypes": null
- },
- {
"kind": "OBJECT",
"name": "ProjectStatistics",
"description": null,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d73972a4622..099779ec3b3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -436,7 +436,7 @@ Represents a ComplianceFramework associated with a Project.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `name` | ProjectSettingEnum! | Name of the compliance framework |
+| `name` | String! | Name of the compliance framework |
### ConfigureSastPayload
@@ -1115,6 +1115,7 @@ Relationship between an epic and an issue.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
+| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
@@ -1311,6 +1312,7 @@ Represents a recorded measurement (object count) for the Admins.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
+| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
@@ -3487,6 +3489,8 @@ Values for sorting issues.
| `RELATIVE_POSITION_ASC` | Relative position by ascending order |
| `SEVERITY_ASC` | Severity from less critical to more critical |
| `SEVERITY_DESC` | Severity from more critical to less critical |
+| `SLA_DUE_AT_ASC` | Issues with earliest SLA due time shown first |
+| `SLA_DUE_AT_DESC` | Issues with latest SLA due time shown first |
| `UPDATED_ASC` | Updated at ascending order |
| `UPDATED_DESC` | Updated at descending order |
| `WEIGHT_ASC` | Weight by ascending order |
@@ -3677,18 +3681,6 @@ Values for sorting projects.
| `SUCCESS` | |
| `WAITING_FOR_RESOURCE` | |
-### ProjectSettingEnum
-
-Names of compliance frameworks that can be assigned to a Project.
-
-| Value | Description |
-| ----- | ----------- |
-| `gdpr` | |
-| `hipaa` | |
-| `pci_dss` | |
-| `soc_2` | |
-| `sox` | |
-
### RegistryState
State of a Geo registry.
diff --git a/doc/api/group_level_variables.md b/doc/api/group_level_variables.md
index aa5f0b3db72..6997ebdede4 100644
--- a/doc/api/group_level_variables.md
+++ b/doc/api/group_level_variables.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Group-level Variables API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34519) in GitLab 9.5
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 53c92cf85ec..be2d9993ca0 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Groups API
## List groups
diff --git a/doc/api/import.md b/doc/api/import.md
index 691e042084e..27f5915b206 100644
--- a/doc/api/import.md
+++ b/doc/api/import.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Import API
## Import repository from GitHub
diff --git a/doc/api/instance_clusters.md b/doc/api/instance_clusters.md
index 1108550eee7..45bfc11c03b 100644
--- a/doc/api/instance_clusters.md
+++ b/doc/api/instance_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Instance clusters API
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36001) in GitLab 13.2.
diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md
index 757910d0946..41e2dd7c147 100644
--- a/doc/api/issue_links.md
+++ b/doc/api/issue_links.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Issue links API **(CORE)**
> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.4.
diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md
index 8e2dcc07af8..ed95cbae3a9 100644
--- a/doc/api/issues_statistics.md
+++ b/doc/api/issues_statistics.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Issues Statistics API
Every API call to issues_statistics must be authenticated.
diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md
index f5510f6ee91..54085e6f508 100644
--- a/doc/api/job_artifacts.md
+++ b/doc/api/job_artifacts.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Job Artifacts API
## Get job artifacts
diff --git a/doc/api/license.md b/doc/api/license.md
index dcdf019059b..8c92a46a975 100644
--- a/doc/api/license.md
+++ b/doc/api/license.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# License **(CORE ONLY)**
To interact with license endpoints, you need to authenticate yourself as an
diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md
index 984cfa92d3a..f7f6fbfbc47 100644
--- a/doc/api/managed_licenses.md
+++ b/doc/api/managed_licenses.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Managed Licenses API **(ULTIMATE)**
## List managed licenses
diff --git a/doc/api/members.md b/doc/api/members.md
index 4440b70c512..d616dfdd85c 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Group and project members API
## Valid access levels
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 0792c6d4a3b..f61400dfddb 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Namespaces API
Usernames and groupnames fall under a special category called namespaces.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index aaff28757bb..4416ce11db2 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Notes API
Notes are comments on:
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 8442e371a56..cbe5aa46a5d 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Notification settings API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5632) in GitLab 8.12.
diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md
index 43310570fe8..76c9338e4a0 100644
--- a/doc/api/personal_access_tokens.md
+++ b/doc/api/personal_access_tokens.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Personal access tokens API **(ULTIMATE)**
You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens).
diff --git a/doc/api/resource_label_events.md b/doc/api/resource_label_events.md
index 275614a1449..b088c06b342 100644
--- a/doc/api/resource_label_events.md
+++ b/doc/api/resource_label_events.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Resource label events API
Resource label events keep track about who, when, and which label was added to, or removed from, an issuable.
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 350f992779e..0c4ca5e898f 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# SCIM API **(SILVER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
diff --git a/doc/api/services.md b/doc/api/services.md
index 7c01e43a4d8..02814146417 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Services API
NOTE: **Note:**
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 236cd10a30e..3885d236a72 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Application settings API **(CORE ONLY)**
These API calls allow you to read and modify GitLab instance
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index caa02412a28..914f5fbf42a 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Sidekiq Metrics API **(CORE ONLY)**
> Introduced in GitLab 8.9.
diff --git a/doc/api/statistics.md b/doc/api/statistics.md
index 890c6f68898..6a41a960eba 100644
--- a/doc/api/statistics.md
+++ b/doc/api/statistics.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Application statistics API
## Get current application statistics
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 3e0d2151428..00cd88c88dd 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# System hooks API
All methods require administrator authorization.
diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md
index e579300a2fd..fd0edfce8e5 100644
--- a/doc/api/templates/dockerfiles.md
+++ b/doc/api/templates/dockerfiles.md
@@ -1,4 +1,7 @@
---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 3acd666ad66..b957c582755 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -1,4 +1,7 @@
---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index 4eb3c0f6111..d1044b23306 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -1,4 +1,7 @@
---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
diff --git a/doc/api/users.md b/doc/api/users.md
index beaea689fb7..31e8bb67bd3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Users API
## List users
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index c351c14e24c..2dd4376413b 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# API V3 to API V4
In GitLab 9.0 and later, API V4 is the preferred version to be used.
diff --git a/doc/api/version.md b/doc/api/version.md
index 3c6feaae071..d1582cf63cd 100644
--- a/doc/api/version.md
+++ b/doc/api/version.md
@@ -1,3 +1,9 @@
+---
+stage: none
+group: unassigned
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Version API
> Introduced in GitLab 8.13.
diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md
index 6c672663bae..40b086d12ff 100644
--- a/doc/development/feature_flags/development.md
+++ b/doc/development/feature_flags/development.md
@@ -188,6 +188,30 @@ if Feature.disabled?(:my_feature_flag, project, type: :ops)
end
```
+DANGER: **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
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 7adea5ebcd6..94ed45d053e 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1358,6 +1358,55 @@ X-Gitlab-Event: Deployment Hook
Note that `deployable_id` is the ID of the CI job.
+### Feature Flag events
+
+Triggered when a feature flag is turned on or off.
+
+**Request Header**:
+
+```plaintext
+X-Gitlab-Event: Feature Flag Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "feature_flag",
+ "project": {
+ "id": 1,
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "ci_config_path": null,
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "email": "admin@example.com"
+ },
+ "user_url": "http://example.com/root",
+ "object_attributes": {
+ "id": 6,
+ "name": "test-feature-flag",
+ "description": "test-feature-flag-description",
+ "active": true
+ }
+}
+```
+
## Image URL rewriting
From GitLab 11.2, simple image references are rewritten to use an absolute URL
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 7e3d70a210a..e6ce62a1c6e 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -8,6 +8,8 @@ module API
helpers ::API::Helpers::MembersHelpers
+ feature_category :authentication_and_authorization
+
%w[group project].each do |source_type|
params do
requires :id, type: String, desc: "The #{source_type} ID"
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb
index 44c389d6f94..654d3a48162 100644
--- a/lib/api/admin/ci/variables.rb
+++ b/lib/api/admin/ci/variables.rb
@@ -8,6 +8,8 @@ module API
before { authenticated_as_admin! }
+ feature_category :continuous_integration
+
namespace 'admin' do
namespace 'ci' do
namespace 'variables' do
diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb
index ce1bdd65eff..679e231b283 100644
--- a/lib/api/admin/instance_clusters.rb
+++ b/lib/api/admin/instance_clusters.rb
@@ -5,6 +5,8 @@ module API
class InstanceClusters < ::API::Base
include PaginationParams
+ feature_category :kubernetes_management
+
before do
authenticated_as_admin!
end
diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb
index c2e9de5fb4e..7e561783685 100644
--- a/lib/api/admin/sidekiq.rb
+++ b/lib/api/admin/sidekiq.rb
@@ -5,6 +5,8 @@ module API
class Sidekiq < ::API::Base
before { authenticated_as_admin! }
+ feature_category :not_owned
+
namespace 'admin' do
namespace 'sidekiq' do
namespace 'queues' do
diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb
index 00b495bbc1e..fe498bf611b 100644
--- a/lib/api/appearance.rb
+++ b/lib/api/appearance.rb
@@ -4,6 +4,8 @@ module API
class Appearance < ::API::Base
before { authenticated_as_admin! }
+ feature_category :navigation
+
helpers do
def current_appearance
@current_appearance ||= (::Appearance.current || ::Appearance.new)
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 2afe8763d9d..8b14e16b495 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -5,6 +5,8 @@ module API
class Applications < ::API::Base
before { authenticated_as_admin! }
+ feature_category :authentication_and_authorization
+
resource :applications do
helpers do
def validate_redirect_uri(value)
diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb
index 5a9b9940fcf..a42d89ddf83 100644
--- a/lib/api/avatar.rb
+++ b/lib/api/avatar.rb
@@ -2,6 +2,8 @@
module API
class Avatar < ::API::Base
+ feature_category :users
+
resource :avatar do
desc 'Return avatar url for a user' do
success Entities::Avatar
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 6d40ae8f5ff..8ea4f32d3eb 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -6,9 +6,9 @@ module API
before { authenticate! }
AWARDABLES = [
- { type: 'issue', find_by: :iid },
- { type: 'merge_request', find_by: :iid },
- { type: 'snippet', find_by: :id }
+ { type: 'issue', find_by: :iid, feature_category: :issue_tracking },
+ { type: 'merge_request', find_by: :iid, feature_category: :code_review },
+ { type: 'snippet', find_by: :id, feature_category: :snippets }
].freeze
params do
@@ -34,7 +34,7 @@ module API
params do
use :pagination
end
- get endpoint do
+ get endpoint, feature_category: awardable_params[:feature_category] do
if can_read_awardable?
awards = awardable.award_emoji
present paginate(awards), with: Entities::AwardEmoji
@@ -50,7 +50,7 @@ module API
params do
requires :award_id, type: Integer, desc: 'The ID of the award'
end
- get "#{endpoint}/:award_id" do
+ get "#{endpoint}/:award_id", feature_category: awardable_params[:feature_category] do
if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
else
@@ -65,7 +65,7 @@ module API
params do
requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
end
- post endpoint do
+ post endpoint, feature_category: awardable_params[:feature_category] do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
@@ -84,7 +84,7 @@ module API
params do
requires :award_id, type: Integer, desc: 'The ID of an award emoji'
end
- delete "#{endpoint}/:award_id" do
+ delete "#{endpoint}/:award_id", feature_category: awardable_params[:feature_category] do
award = awardable.award_emoji.find(params[:award_id])
unauthorized! unless award.user == current_user || current_user.admin?
diff --git a/lib/gitlab/data_builder/feature_flag.rb b/lib/gitlab/data_builder/feature_flag.rb
new file mode 100644
index 00000000000..2f675ace7e1
--- /dev/null
+++ b/lib/gitlab/data_builder/feature_flag.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DataBuilder
+ module FeatureFlag
+ extend self
+
+ def build(feature_flag, user)
+ {
+ object_kind: 'feature_flag',
+ project: feature_flag.project.hook_attrs,
+ user: user.hook_attrs,
+ user_url: Gitlab::UrlBuilder.build(user),
+ object_attributes: feature_flag.hook_attrs
+ }
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 87240e5a50f..c3925914ca0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9223,6 +9223,15 @@ msgstr ""
msgid "DevOps Score"
msgstr ""
+msgid "DevopsAdoption|Add a segment to get started"
+msgstr ""
+
+msgid "DevopsAdoption|Add new segment"
+msgstr ""
+
+msgid "DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team."
+msgstr ""
+
msgid "Diff content limits"
msgstr ""
@@ -29704,6 +29713,9 @@ msgstr ""
msgid "Webhooks|Enable SSL verification"
msgstr ""
+msgid "Webhooks|Feature Flag events"
+msgstr ""
+
msgid "Webhooks|Issues events"
msgstr ""
@@ -29731,6 +29743,9 @@ msgstr ""
msgid "Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled"
msgstr ""
+msgid "Webhooks|This URL is triggered when a feature flag is turned on or off"
+msgstr ""
+
msgid "Webhooks|This URL will be triggered by a push to the repository"
msgstr ""
diff --git a/public/robots.txt b/public/robots.txt
index d4183c5cafb..9b943e6a1cb 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -23,6 +23,7 @@ Disallow: /users
Disallow: /help
Disallow: /s/
Disallow: /-/profile
+Disallow: /-/ide/
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in
diff --git a/scripts/generate-test-mapping b/scripts/generate-test-mapping
new file mode 100755
index 00000000000..eabe6a5b513
--- /dev/null
+++ b/scripts/generate-test-mapping
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+
+require 'json'
+require_relative '../tooling/lib/tooling/test_map_generator'
+
+test_mapping_json = ARGV.shift
+crystalball_yamls = ARGV
+
+unless test_mapping_json && !crystalball_yamls.empty?
+ puts "usage: #{__FILE__} <test_mapping_json> [crystalball_yamls...]"
+ exit 1
+end
+
+map_generator = Tooling::TestMapGenerator.new
+map_generator.parse(crystalball_yamls)
+mapping = map_generator.mapping
+
+File.write(test_mapping_json, JSON.pretty_generate(mapping))
+puts "Saved #{test_mapping_json}."
diff --git a/scripts/pack-test-mapping b/scripts/pack-test-mapping
new file mode 100755
index 00000000000..58ace3eca67
--- /dev/null
+++ b/scripts/pack-test-mapping
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+
+require 'json'
+require_relative '../tooling/lib/tooling/test_map_packer'
+
+unpacked_json_mapping, packed_json_mapping = ARGV.shift(2)
+unless packed_json_mapping && unpacked_json_mapping
+ puts "usage: #{__FILE__} <unpacked_json_mapping> <packed_json_mapping>"
+ exit 1
+end
+
+puts "Compressing #{unpacked_json_mapping}"
+
+mapping = JSON.parse(File.read(unpacked_json_mapping))
+packed_mapping = Tooling::TestMapPacker.new.pack(mapping)
+
+puts "Writing packed #{packed_json_mapping}"
+File.write(packed_json_mapping, JSON.generate(packed_mapping))
+puts "Saved #{packed_json_mapping}."
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 3812a8b8ef7..9fe7d089d93 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -48,6 +48,43 @@ function update_tests_metadata() {
fi
}
+function retrieve_tests_mapping() {
+ mkdir -p crystalball/
+
+ if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
+ (wget -O "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" "http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
+ fi
+
+ scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
+}
+
+function update_tests_mapping() {
+ if ! crystalball_rspec_data_exists; then
+ echo "No crystalball rspec data found."
+ return 0
+ fi
+
+ scripts/generate-test-mapping "${RSPEC_TESTS_MAPPING_PATH}" crystalball/rspec*.yml
+
+ scripts/pack-test-mapping "${RSPEC_TESTS_MAPPING_PATH}" "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
+
+ gzip "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
+
+ if [[ -n "${TESTS_METADATA_S3_BUCKET}" ]]; then
+ if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
+ scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz"
+ else
+ echo "Not uploading report to S3 as the pipeline is not a scheduled one."
+ fi
+ fi
+
+ rm -f crystalball/rspec*.yml
+}
+
+function crystalball_rspec_data_exists() {
+ compgen -G "crystalball/rspec*.yml" > /dev/null;
+}
+
function rspec_simple_job() {
local rspec_opts="${1}"
diff --git a/scripts/unpack-test-mapping b/scripts/unpack-test-mapping
new file mode 100755
index 00000000000..c0f706c3f9f
--- /dev/null
+++ b/scripts/unpack-test-mapping
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+require 'json'
+require_relative '../tooling/lib/tooling/test_map_packer'
+
+packed_json_mapping, unpacked_json_mapping = ARGV.shift(2)
+unless packed_json_mapping && unpacked_json_mapping
+ puts "usage: #{__FILE__} <packed_json_mapping> <unpacked_json_mapping>"
+ exit 1
+end
+
+packed_mapping = JSON.parse(File.read(packed_json_mapping))
+mapping = Tooling::TestMapPacker.new.unpack(packed_mapping)
+
+puts "Writing unpacked #{unpacked_json_mapping}"
+File.write(unpacked_json_mapping, JSON.generate(mapping))
+puts "Saved #{unpacked_json_mapping}."
diff --git a/spec/crystalball_env.rb b/spec/crystalball_env.rb
new file mode 100644
index 00000000000..56498f07f85
--- /dev/null
+++ b/spec/crystalball_env.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module CrystalballEnv
+ EXCLUDED_PREFIXES = %w[vendor/ruby].freeze
+
+ extend self
+
+ def start!
+ return unless ENV['CRYSTALBALL'] && ENV['CI_PIPELINE_SOURCE'] == 'schedule'
+
+ require 'crystalball'
+ require_relative '../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
+ require_relative '../tooling/lib/tooling/crystalball/coverage_lines_strategy'
+
+ map_storage_path_base = ENV['CI_JOB_NAME'] || 'crystalball_data'
+ map_storage_path = "crystalball/#{map_storage_path_base.gsub(%r{[/ ]}, '_')}.yml"
+
+ execution_detector = Tooling::Crystalball::CoverageLinesExecutionDetector.new(exclude_prefixes: EXCLUDED_PREFIXES)
+
+ Crystalball::MapGenerator.start! do |config|
+ config.map_storage_path = map_storage_path
+ config.register Tooling::Crystalball::CoverageLinesStrategy.new(execution_detector)
+ end
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 15b240acba4..88b5ff936fe 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -22,6 +22,7 @@ FactoryBot.define do
pipeline_events { true }
wiki_page_events { true }
deployment_events { true }
+ feature_flag_events { true }
end
end
end
diff --git a/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js b/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js
new file mode 100644
index 00000000000..978a358af43
--- /dev/null
+++ b/spec/frontend/admin/dev_ops_report/components/devops_adoption_app_spec.js
@@ -0,0 +1,21 @@
+import { shallowMount } from '@vue/test-utils';
+import DevopsAdoptionApp from '~/admin/dev_ops_report/components/devops_adoption_app.vue';
+import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue';
+
+describe('DevopsAdoptionApp', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ return shallowMount(DevopsAdoptionApp);
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('default behaviour', () => {
+ it('displays the empty state', () => {
+ expect(wrapper.find(DevopsAdoptionEmptyState).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js b/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js
new file mode 100644
index 00000000000..91e99e6dffa
--- /dev/null
+++ b/spec/frontend/admin/dev_ops_report/components/devops_adoption_empty_state_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue';
+import { DEVOPS_ADOPTION_STRINGS } from '~/admin/dev_ops_report/constants';
+
+const emptyStateSvgPath = 'illustrations/monitoring/getting_started.svg';
+
+describe('DevopsAdoptionEmptyState', () => {
+ let wrapper;
+
+ const createComponent = (options = {}) => {
+ const { stubs = {} } = options;
+ return shallowMount(DevopsAdoptionEmptyState, {
+ provide: {
+ emptyStateSvgPath,
+ },
+ stubs,
+ });
+ };
+
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findEmptyStateAction = () => findEmptyState().find(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('contains the correct svg', () => {
+ wrapper = createComponent();
+
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ });
+
+ it('contains the correct text', () => {
+ wrapper = createComponent();
+
+ const emptyState = findEmptyState();
+
+ expect(emptyState.props('title')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.title);
+ expect(emptyState.props('description')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.description);
+ });
+
+ it('contains an overridden action button', () => {
+ wrapper = createComponent({ stubs: { GlEmptyState } });
+
+ const actionButton = findEmptyStateAction();
+
+ expect(actionButton.exists()).toBe(true);
+ expect(actionButton.text()).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.button);
+ });
+});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 709f66bb352..6329a84ff6e 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -10,6 +10,7 @@ import {
TH_CREATED_AT_TEST_ID,
TH_SEVERITY_TEST_ID,
TH_PUBLISHED_TEST_ID,
+ TH_INCIDENT_SLA_TEST_ID,
trackIncidentCreateNewOptions,
trackIncidentListViewsOptions,
} from '~/incidents/constants';
@@ -277,10 +278,11 @@ describe('Incidents List', () => {
const noneSort = 'none';
it.each`
- selector | initialSort | firstSort | nextSort
- ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
- ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
- ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ selector | initialSort | firstSort | nextSort
+ ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
+ ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
`('updates sort with new direction', async ({ selector, initialSort, firstSort, nextSort }) => {
const [[attr, value]] = Object.entries(selector);
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 08b25d64b43..dde8f3b587f 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -370,6 +370,26 @@ RSpec.describe GroupsHelper do
end
end
+ describe '#show_thanks_for_purchase_banner?' do
+ subject { helper.show_thanks_for_purchase_banner? }
+
+ it 'returns true with purchased_quantity present in params' do
+ allow(controller).to receive(:params) { { purchased_quantity: '1' } }
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false with purchased_quantity not present in params' do
+ is_expected.to be_falsey
+ end
+
+ it 'returns false with purchased_quantity is empty in params' do
+ allow(controller).to receive(:params) { { purchased_quantity: '' } }
+
+ is_expected.to be_falsey
+ end
+ end
+
describe '#show_invite_banner?' do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) }
diff --git a/spec/lib/api/every_api_endpoint_spec.rb b/spec/lib/api/every_api_endpoint_spec.rb
index a0657b5fbed..2fcb09578c4 100644
--- a/spec/lib/api/every_api_endpoint_spec.rb
+++ b/spec/lib/api/every_api_endpoint_spec.rb
@@ -17,8 +17,14 @@ RSpec.describe 'Every API endpoint' do
let_it_be(:routes_without_category) do
api_endpoints.map do |(klass, path)|
next if klass.try(:feature_category_for_action, path)
+
# We'll add the rest in https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463
- next unless klass == ::API::Users || klass == ::API::Issues
+ completed_classes = [
+ ::API::Users, ::API::Issues, ::API::AccessRequests, ::API::Admin::Ci::Variables,
+ ::API::Admin::InstanceClusters, ::API::Admin::Sidekiq, ::API::Appearance,
+ ::API::Applications, ::API::Avatar, ::API::AwardEmoji
+ ]
+ next unless completed_classes.include?(klass)
"#{klass}##{path}"
end.compact.uniq
diff --git a/spec/lib/gitlab/data_builder/feature_flag_spec.rb b/spec/lib/gitlab/data_builder/feature_flag_spec.rb
new file mode 100644
index 00000000000..75511fcf9f5
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/feature_flag_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DataBuilder::FeatureFlag do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ describe '.build' do
+ let(:data) { described_class.build(feature_flag, user) }
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:object_kind]).to eq('feature_flag') }
+
+ it 'contains the correct object attributes' do
+ object_attributes = data[:object_attributes]
+
+ expect(object_attributes[:id]).to eq(feature_flag.id)
+ expect(object_attributes[:name]).to eq(feature_flag.name)
+ expect(object_attributes[:description]).to eq(feature_flag.description)
+ expect(object_attributes[:active]).to eq(feature_flag.active)
+ end
+ end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index fc463c6af52..c4d17905637 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -161,6 +161,12 @@ RSpec.describe BroadcastMessage do
expect(subject.call('/group/issues/test').length).to eq(1)
end
+
+ it "does not return message if the target path is set but no current path is provided" do
+ create(:broadcast_message, target_path: "*/issues/*", broadcast_type: broadcast_type)
+
+ expect(subject.call.length).to eq(0)
+ end
end
describe '.current', :use_clean_rails_memory_store_caching do
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index b4e941f2856..93dd7d4f0bb 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -261,4 +261,38 @@ RSpec.describe Operations::FeatureFlag do
expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id])
end
end
+
+ describe '#hook_attrs' do
+ it 'includes expected attributes' do
+ hook_attrs = {
+ id: subject.id,
+ name: subject.name,
+ description: subject.description,
+ active: subject.active
+ }
+ expect(subject.hook_attrs).to eq(hook_attrs)
+ end
+ end
+
+ describe "#execute_hooks" do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ it 'does not execute the hook when feature_flag event is disabled' do
+ create(:project_hook, project: project, feature_flag_events: false)
+ expect(WebHookWorker).not_to receive(:perform_async)
+
+ feature_flag.execute_hooks(user)
+ feature_flag.touch
+ end
+
+ it 'executes hook when feature_flag event is enabled' do
+ hook = create(:project_hook, project: project, feature_flag_events: true)
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, an_instance_of(Hash), 'feature_flag_hooks')
+
+ feature_flag.execute_hooks(user)
+ feature_flag.touch
+ end
+ end
end
diff --git a/spec/requests/robots_txt_spec.rb b/spec/requests/robots_txt_spec.rb
index 61a5dc68fdd..e3f279af3c8 100644
--- a/spec/requests/robots_txt_spec.rb
+++ b/spec/requests/robots_txt_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
'/help',
'/s/',
'/-/profile',
+ '/-/ide/project',
'/foo/bar/new',
'/foo/bar/edit',
'/foo/bar/raw',
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index a982dd5166b..66a75a2c24e 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -100,6 +100,13 @@ RSpec.describe FeatureFlags::UpdateService do
include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.')
)
end
+
+ it 'executes hooks' do
+ hook = create(:project_hook, :all_events_enabled, project: project)
+ expect(WebHookWorker).to receive(:perform_async).with(hook.id, an_instance_of(Hash), 'feature_flag_hooks')
+
+ subject
+ end
end
context 'when scope active state is changed' do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 11a45e005b8..98ce765100b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,9 @@
require './spec/simplecov_env'
SimpleCovEnv.start!
+require './spec/crystalball_env'
+CrystalballEnv.start!
+
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
diff --git a/spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb b/spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb
new file mode 100644
index 00000000000..6b7373cb3c7
--- /dev/null
+++ b/spec/tooling/lib/tooling/crystalball/coverage_lines_execution_detector_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative '../../../../../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
+
+RSpec.describe Tooling::Crystalball::CoverageLinesExecutionDetector do
+ subject(:detector) { described_class.new(root, exclude_prefixes: %w[vendor/ruby]) }
+
+ let(:root) { '/tmp' }
+ let(:before_map) { { path => { lines: [0, 2, nil] } } }
+ let(:after_map) { { path => { lines: [0, 3, nil] } } }
+ let(:path) { '/tmp/file.rb' }
+
+ describe '#detect' do
+ subject { detector.detect(before_map, after_map) }
+
+ it { is_expected.to eq(%w[file.rb]) }
+
+ context 'with no changes' do
+ let(:after_map) { { path => { lines: [0, 2, nil] } } }
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'with previously uncovered file' do
+ let(:before_map) { {} }
+
+ it { is_expected.to eq(%w[file.rb]) }
+ end
+
+ context 'with path outside of root' do
+ let(:path) { '/abc/file.rb' }
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'with path in excluded prefix' do
+ let(:path) { '/tmp/vendor/ruby/dependency.rb' }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb b/spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb
new file mode 100644
index 00000000000..fd8fc4114a1
--- /dev/null
+++ b/spec/tooling/lib/tooling/crystalball/coverage_lines_strategy_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative '../../../../../tooling/lib/tooling/crystalball/coverage_lines_strategy'
+
+RSpec.describe Tooling::Crystalball::CoverageLinesStrategy do
+ subject { described_class.new(execution_detector) }
+
+ let(:execution_detector) { instance_double('Tooling::Crystalball::CoverageLinesExecutionDetector') }
+
+ describe '#after_register' do
+ it 'starts coverage' do
+ expect(Coverage).to receive(:start).with(lines: true)
+ subject.after_register
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/test_map_generator_spec.rb b/spec/tooling/lib/tooling/test_map_generator_spec.rb
new file mode 100644
index 00000000000..7f3b2807162
--- /dev/null
+++ b/spec/tooling/lib/tooling/test_map_generator_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative '../../../../tooling/lib/tooling/test_map_generator'
+
+RSpec.describe Tooling::TestMapGenerator do
+ subject { described_class.new }
+
+ describe '#parse' do
+ let(:yaml1) do
+ <<~YAML
+ ---
+ :type: Crystalball::ExecutionMap
+ :commit: a7d57d333042f3b0334b2f8a282354eef7365976
+ :timestamp: 1602668405
+ :version:
+ ---
+ "./spec/factories_spec.rb[1]":
+ - lib/gitlab/current_settings.rb
+ - lib/feature.rb
+ - lib/gitlab/marginalia.rb
+ YAML
+ end
+
+ let(:yaml2) do
+ <<~YAML
+ ---
+ :type: Crystalball::ExecutionMap
+ :commit: 74056e8d9cf3773f43faa1cf5416f8779c8284c8
+ :timestamp: 1602671965
+ :version:
+ ---
+ "./spec/models/project_spec.rb[1]":
+ - lib/gitlab/current_settings.rb
+ - lib/feature.rb
+ - lib/gitlab/marginalia.rb
+ YAML
+ end
+
+ let(:pathname) { instance_double(Pathname) }
+
+ before do
+ allow(File).to receive(:read).with('yaml1.yml').and_return(yaml1)
+ allow(File).to receive(:read).with('yaml2.yml').and_return(yaml2)
+ end
+
+ context 'with single yaml' do
+ let(:expected_mapping) do
+ {
+ 'lib/gitlab/current_settings.rb' => [
+ './spec/factories_spec.rb'
+ ],
+ 'lib/feature.rb' => [
+ './spec/factories_spec.rb'
+ ],
+ 'lib/gitlab/marginalia.rb' => [
+ './spec/factories_spec.rb'
+ ]
+ }
+ end
+
+ it 'parses crystalball data into test mapping' do
+ subject.parse('yaml1.yml')
+
+ expect(subject.mapping.keys).to match_array(expected_mapping.keys)
+ end
+
+ it 'stores test files without example uid' do
+ subject.parse('yaml1.yml')
+
+ expected_mapping.each do |file, tests|
+ expect(subject.mapping[file]).to match_array(tests)
+ end
+ end
+ end
+
+ context 'with multiple yamls' do
+ let(:expected_mapping) do
+ {
+ 'lib/gitlab/current_settings.rb' => [
+ './spec/factories_spec.rb',
+ './spec/models/project_spec.rb'
+ ],
+ 'lib/feature.rb' => [
+ './spec/factories_spec.rb',
+ './spec/models/project_spec.rb'
+ ],
+ 'lib/gitlab/marginalia.rb' => [
+ './spec/factories_spec.rb',
+ './spec/models/project_spec.rb'
+ ]
+ }
+ end
+
+ it 'parses crystalball data into test mapping' do
+ subject.parse(%w[yaml1.yml yaml2.yml])
+
+ expect(subject.mapping.keys).to match_array(expected_mapping.keys)
+ end
+
+ it 'stores test files without example uid' do
+ subject.parse(%w[yaml1.yml yaml2.yml])
+
+ expected_mapping.each do |file, tests|
+ expect(subject.mapping[file]).to match_array(tests)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/test_map_packer_spec.rb b/spec/tooling/lib/tooling/test_map_packer_spec.rb
new file mode 100644
index 00000000000..233134d2524
--- /dev/null
+++ b/spec/tooling/lib/tooling/test_map_packer_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require_relative '../../../../tooling/lib/tooling/test_map_packer'
+
+RSpec.describe Tooling::TestMapPacker do
+ subject { described_class.new }
+
+ let(:map) do
+ {
+ 'file1.rb' => [
+ './a/b/c/test_1.rb',
+ './a/b/test_2.rb',
+ './a/b/test_3.rb',
+ './a/test_4.rb',
+ './test_5.rb'
+ ],
+ 'file2.rb' => [
+ './a/b/c/test_1.rb',
+ './a/test_4.rb',
+ './test_5.rb'
+ ]
+ }
+ end
+
+ let(:compact_map) do
+ {
+ 'file1.rb' => {
+ '.' => {
+ 'a' => {
+ 'b' => {
+ 'c' => {
+ 'test_1.rb' => 1
+ },
+ 'test_2.rb' => 1,
+ 'test_3.rb' => 1
+ },
+ 'test_4.rb' => 1
+ },
+ 'test_5.rb' => 1
+ }
+ },
+ 'file2.rb' => {
+ '.' => {
+ 'a' => {
+ 'b' => {
+ 'c' => {
+ 'test_1.rb' => 1
+ }
+ },
+ 'test_4.rb' => 1
+ },
+ 'test_5.rb' => 1
+ }
+ }
+ }
+ end
+
+ describe '#pack' do
+ it 'compacts list of test files into a prefix tree' do
+ expect(subject.pack(map)).to eq(compact_map)
+ end
+
+ it 'does nothing to empty hash' do
+ expect(subject.pack({})).to eq({})
+ end
+ end
+
+ describe '#unpack' do
+ it 'unpack prefix tree into list of test files' do
+ expect(subject.unpack(compact_map)).to eq(map)
+ end
+
+ it 'does nothing to empty hash' do
+ expect(subject.unpack({})).to eq({})
+ end
+ end
+end
diff --git a/tooling/lib/tooling/crystalball/coverage_lines_execution_detector.rb b/tooling/lib/tooling/crystalball/coverage_lines_execution_detector.rb
new file mode 100644
index 00000000000..47ddf568fe4
--- /dev/null
+++ b/tooling/lib/tooling/crystalball/coverage_lines_execution_detector.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'crystalball/map_generator/helpers/path_filter'
+
+module Tooling
+ module Crystalball
+ # Class for detecting code execution path based on coverage information diff
+ class CoverageLinesExecutionDetector
+ include ::Crystalball::MapGenerator::Helpers::PathFilter
+
+ attr_reader :exclude_prefixes
+
+ def initialize(*args, exclude_prefixes: [])
+ super(*args)
+ @exclude_prefixes = exclude_prefixes
+ end
+
+ # Detects files affected during example execution based on line coverage.
+ # Transforms absolute paths to relative.
+ # Exclude paths outside of repository and in excluded prefixes
+ #
+ # @param[Hash] hash of files affected before example execution
+ # @param[Hash] hash of files affected after example execution
+ # @return [Array<String>]
+ def detect(before, after)
+ file_names = after.keys
+ covered_files = file_names.reject { |file_name| same_coverage?(before, after, file_name) }
+ filter(covered_files)
+ end
+
+ private
+
+ def same_coverage?(before, after, file_name)
+ before[file_name] && before[file_name][:lines] == after[file_name][:lines]
+ end
+
+ def filter(paths)
+ super.reject do |file_name|
+ exclude_prefixes.any? { |prefix| file_name.start_with?(prefix) }
+ end
+ end
+ end
+ end
+end
diff --git a/tooling/lib/tooling/crystalball/coverage_lines_strategy.rb b/tooling/lib/tooling/crystalball/coverage_lines_strategy.rb
new file mode 100644
index 00000000000..ebcaab0b8d8
--- /dev/null
+++ b/tooling/lib/tooling/crystalball/coverage_lines_strategy.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'coverage'
+require 'crystalball/map_generator/coverage_strategy'
+require_relative './coverage_lines_execution_detector'
+
+module Tooling
+ module Crystalball
+ # Crystalball map generator strategy based on Crystalball::MapGenerator::CoverageStrategy,
+ # modified to use Coverage.start(lines: true)
+ # This maintains compatibility with SimpleCov on Ruby >= 2.5 with start arguments
+ # and SimpleCov.start uses Coverage.start(lines: true) by default
+ class CoverageLinesStrategy < ::Crystalball::MapGenerator::CoverageStrategy
+ def initialize(execution_detector = CoverageLinesExecutionDetector)
+ super(execution_detector)
+ end
+
+ def after_register
+ Coverage.start(lines: true)
+ end
+ end
+ end
+end
diff --git a/tooling/lib/tooling/test_map_generator.rb b/tooling/lib/tooling/test_map_generator.rb
new file mode 100644
index 00000000000..bd0415f6e67
--- /dev/null
+++ b/tooling/lib/tooling/test_map_generator.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'set'
+require 'yaml'
+
+module Tooling
+ class TestMapGenerator
+ def initialize
+ @mapping = Hash.new { |h, k| h[k] = Set.new }
+ end
+
+ def parse(yaml_files)
+ Array(yaml_files).each do |yaml_file|
+ data = File.read(yaml_file)
+ _metadata, example_groups = data.split("---\n").reject(&:empty?).map { |yml| YAML.safe_load(yml, [Symbol]) }
+
+ example_groups.each do |example_id, files|
+ files.each do |file|
+ spec_file = strip_example_uid(example_id)
+ @mapping[file] << spec_file
+ end
+ end
+ end
+ end
+
+ def mapping
+ @mapping.transform_values { |set| set.to_a }
+ end
+
+ private
+
+ def strip_example_uid(example_id)
+ example_id.gsub(/\[.+\]/, '')
+ end
+ end
+end
diff --git a/tooling/lib/tooling/test_map_packer.rb b/tooling/lib/tooling/test_map_packer.rb
new file mode 100644
index 00000000000..520d69610eb
--- /dev/null
+++ b/tooling/lib/tooling/test_map_packer.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Tooling
+ class TestMapPacker
+ SEPARATOR = '/'.freeze
+ MARKER = 1
+
+ def pack(map)
+ map.transform_values(&method(:create_tree_from_tests))
+ end
+
+ def unpack(compact_map)
+ compact_map.transform_values(&method(:retrieve_tests_from_tree))
+ end
+
+ private
+
+ def create_tree_from_tests(tests)
+ tests.inject({}) do |tree, test|
+ segments = test.split(SEPARATOR)
+ branch = create_branch_from_segments(segments)
+ deep_merge(tree, branch)
+ end
+ end
+
+ def create_branch_from_segments(segments)
+ segments.reverse.inject(MARKER) { |node, parent| { parent => node } }
+ end
+
+ def deep_merge(hash, other)
+ hash.merge(other) do |_, this_val, other_val|
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
+ deep_merge(this_val, other_val)
+ else
+ other_val
+ end
+ end
+ end
+
+ def retrieve_tests_from_tree(tree)
+ traverse(tree).inject([]) do |tests, test|
+ tests << test
+ end
+ end
+
+ def traverse(tree, segments = [], &block)
+ return to_enum(__method__, tree, segments) unless block_given?
+
+ if tree == MARKER
+ return yield segments.join(SEPARATOR)
+ end
+
+ tree.each do |key, value|
+ traverse(value, segments + [key], &block)
+ end
+ end
+ end
+end