Commit 26f4a225 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 49e49cfb 8ad16eb9
...@@ -73,6 +73,7 @@ eslint-report.html ...@@ -73,6 +73,7 @@ eslint-report.html
/.gitlab_pages_secret /.gitlab_pages_secret
/.gitlab_kas_secret /.gitlab_kas_secret
/webpack-report/ /webpack-report/
/crystalball/
/knapsack/ /knapsack/
/rspec_flaky/ /rspec_flaky/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
......
...@@ -59,6 +59,8 @@ variables: ...@@ -59,6 +59,8 @@ variables:
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.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" BUILD_ASSETS_IMAGE: "false"
ES_JAVA_OPTS: "-Xms256m -Xmx256m" ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200" ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
variables: variables:
RUBY_GC_MALLOC_LIMIT: 67108864 RUBY_GC_MALLOC_LIMIT: 67108864
RUBY_GC_MALLOC_LIMIT_MAX: 134217728 RUBY_GC_MALLOC_LIMIT_MAX: 134217728
CRYSTALBALL: "true"
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"] needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"]
script: script:
- *base-script - *base-script
...@@ -29,6 +30,7 @@ ...@@ -29,6 +30,7 @@
when: always when: always
paths: paths:
- coverage/ - coverage/
- crystalball/
- knapsack/ - knapsack/
- rspec_flaky/ - rspec_flaky/
- rspec_profiling/ - rspec_profiling/
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
- knapsack/ - knapsack/
- rspec_flaky/ - rspec_flaky/
- rspec_profiling/ - rspec_profiling/
- crystalball/
retrieve-tests-metadata: retrieve-tests-metadata:
extends: extends:
...@@ -41,3 +42,4 @@ update-tests-metadata: ...@@ -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" - 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 - source ./scripts/rspec_helpers.sh
- update_tests_metadata - update_tests_metadata
- update_tests_mapping
322d3608e68f6ed1dc2efa9164f3026006f43e12 cf8e99ccc104f0a43f41e54896ee46a5e1b15a0a
...@@ -386,6 +386,7 @@ group :development, :test do ...@@ -386,6 +386,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'knapsack', '~> 1.17' gem 'knapsack', '~> 1.17'
gem 'crystalball', '~> 0.7.0', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false gem 'simple_po_parser', '~> 1.1.2', require: false
......
...@@ -199,6 +199,8 @@ GEM ...@@ -199,6 +199,8 @@ GEM
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.6) crass (1.0.6)
creole (0.5.0) creole (0.5.0)
crystalball (0.7.0)
git
css_parser (1.7.0) css_parser (1.7.0)
addressable addressable
daemons (1.2.6) daemons (1.2.6)
...@@ -1291,6 +1293,7 @@ DEPENDENCIES ...@@ -1291,6 +1293,7 @@ DEPENDENCIES
connection_pool (~> 2.0) connection_pool (~> 2.0)
countries (~> 3.0) countries (~> 3.0)
creole (~> 0.5.0) creole (~> 0.5.0)
crystalball (~> 0.7.0)
danger (~> 8.0.6) danger (~> 8.0.6)
database_cleaner (~> 1.7.0) database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.3.1) deckar01-task_list (= 2.3.1)
......
<script>
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
export default {
name: 'DevopsAdoptionApp',
components: {
DevopsAdoptionEmptyState,
},
};
</script>
<template>
<devops-adoption-empty-state />
</template>
<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>
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'),
},
};
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);
},
});
};
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);
},
});
};
import Vue from 'vue'; import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
import UserCallout from '~/user_callout'; import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue';
document.addEventListener('DOMContentLoaded', () => { initDevOpsScoreEmptyState();
// eslint-disable-next-line no-new initDevopAdoption();
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);
},
});
});
...@@ -174,6 +174,10 @@ module GroupsHelper ...@@ -174,6 +174,10 @@ module GroupsHelper
!multiple_members?(group) !multiple_members?(group)
end end
def show_thanks_for_purchase_banner?
params.key?(:purchased_quantity) && params[:purchased_quantity].to_i > 0
end
private private
def just_created? def just_created?
......
...@@ -103,6 +103,7 @@ class BroadcastMessage < ApplicationRecord ...@@ -103,6 +103,7 @@ class BroadcastMessage < ApplicationRecord
end end
def matches_current_path(current_path) def matches_current_path(current_path)
return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank? return true if current_path.blank? || target_path.blank?
escaped = Regexp.escape(target_path).gsub('\\*', '.*') escaped = Regexp.escape(target_path).gsub('\\*', '.*')
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.tab-pane.active#devops_score_pane .tab-pane.active#devops_score_pane
= render 'report' = render 'report'
.tab-pane#devops_adoption_pane .tab-pane#devops_adoption_pane
.js-devops-adoption .js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } }
- else - else
= render 'report' = render 'report'
- breadcrumb_title _("Details") - breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout - @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) - if show_invite_banner?(@group)
= content_for :group_invite_members_banner do = content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" } .container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
......
---
title: Disallow WebIDE route in robots.txt
merge_request: 46117
author:
type: changed
---
title: Don't return target-specific broadcasts without a current path supplied
merge_request: 46322
author:
type: fixed
---
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 # API Docs
Automate GitLab via a simple and powerful API. Automate GitLab via a simple and powerful API.
......
---
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 # Group-level Variables API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34519) in GitLab 9.5 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34519) in GitLab 9.5
......
---
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 # Groups API
## List groups ## List groups
......
---
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 API
## Import repository from GitHub ## Import repository from GitHub
......
---
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 # Instance clusters API
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36001) in GitLab 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36001) in GitLab 13.2.
......
---
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)** # 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. > 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.
......
---
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 # Issues Statistics API
Every API call to issues_statistics must be authenticated. Every API call to issues_statistics must be authenticated.
......
---
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 # Job Artifacts API
## Get job artifacts ## Get job artifacts
......
---
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)** # License **(CORE ONLY)**
To interact with license endpoints, you need to authenticate yourself as an To interact with license endpoints, you need to authenticate yourself as an
......
---
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)** # Managed Licenses API **(ULTIMATE)**
## List managed licenses ## List managed licenses
......
---
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 # Group and project members API
## Valid access levels ## Valid access levels
......
---
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 # Namespaces API
Usernames and groupnames fall under a special category called namespaces. Usernames and groupnames fall under a special category called namespaces.
......
---
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 API
Notes are comments on: Notes are comments on:
......
---
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 # Notification settings API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5632) in GitLab 8.12. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5632) in GitLab 8.12.
......
---
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)** # Personal access tokens API **(ULTIMATE)**
You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens). You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens).
......
---
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 API
Resource label events keep track about who, when, and which label was added to, or removed from, an issuable. Resource label events keep track about who, when, and which label was added to, or removed from, an issuable.
......
---
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)** # SCIM API **(SILVER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
......
---
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 # Services API
NOTE: **Note:** NOTE: **Note:**
......
---
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)** # Application settings API **(CORE ONLY)**
These API calls allow you to read and modify GitLab instance These API calls allow you to read and modify GitLab instance
......
---
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)** # Sidekiq Metrics API **(CORE ONLY)**
> Introduced in GitLab 8.9. > Introduced in GitLab 8.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 # Application statistics API
## Get current application statistics ## Get current application statistics
......
---
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 # System hooks API
All methods require administrator authorization. All methods require administrator authorization.
......
--- ---
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 type: reference
--- ---
......
--- ---
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 type: reference
--- ---
......
--- ---
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 type: reference
--- ---
......
---
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 # Users API
## List users ## List users
......
---
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 # API V3 to API V4
In GitLab 9.0 and later, API V4 is the preferred version to be used. In GitLab 9.0 and later, API V4 is the preferred version to be used.
......
---
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 # Version API
> Introduced in GitLab 8.13. > Introduced in GitLab 8.13.
......
...@@ -188,6 +188,30 @@ if Feature.disabled?(:my_feature_flag, project, type: :ops) ...@@ -188,6 +188,30 @@ if Feature.disabled?(:my_feature_flag, project, type: :ops)
end 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 ### Frontend
Use the `push_frontend_feature_flag` method for frontend code, which is Use the `push_frontend_feature_flag` method for frontend code, which is
......
...@@ -60,10 +60,10 @@ class SubscriptionsController < ApplicationController ...@@ -60,10 +60,10 @@ class SubscriptionsController < ApplicationController
).execute ).execute
if response[:success] if response[:success]
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
redirect_location = if params[:selected_group] redirect_location = if params[:selected_group]
group_path(group) group_path(group, plan_id: plan_id, purchased_quantity: quantity)
else else
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
edit_subscriptions_group_path(group.path, plan_id: plan_id, quantity: quantity, new_user: params[:new_user]) edit_subscriptions_group_path(group.path, plan_id: plan_id, quantity: quantity, new_user: params[:new_user])
end end
......
%section.gl-banner.gl-banner-introduction.gl-p-3.px-lg-6
.gl-banner-illustration.gl-display-flex
= image_tag('illustrations/illustration-congratulation-purchase.svg', class: 'mw-xs')
.gl-banner-content.gl-display-flex.gl-flex-direction-column.gl-justify-content-center
%h3= _('Thanks for your purchase!')
- number_of_users = n_('1 user', '%{num} users', quantity) % { num: quantity }
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
...@@ -2,13 +2,7 @@ ...@@ -2,13 +2,7 @@
.row.flex-grow-1.bg-gray-light .row.flex-grow-1.bg-gray-light
.d-flex.flex-column.align-items-center.w-100.gl-p-5 .d-flex.flex-column.align-items-center.w-100.gl-p-5
%section.gl-banner.gl-banner-introduction.gl-p-3.px-lg-6 = render 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:quantity].to_i
.gl-banner-illustration.d-flex
= image_tag('illustrations/subscription-success.svg', class: 'mw-xs')
.gl-banner-content.d-flex.flex-column.justify-content-center
%h3= _('Thanks for your purchase!')
- number_of_users = n_('1 user', '%{num} users', params[:quantity].to_i) % { num: params[:quantity] }
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
.edit-group.d-flex.flex-column.align-items-center.gl-pt-7 .edit-group.d-flex.flex-column.align-items-center.gl-pt-7
- if params[:new_user] - if params[:new_user]
#progress-bar #progress-bar
......
---
title: Show purchase confirmation banner when user makes new purchases for existing
groups
merge_request: 45786
author:
type: fixed
...@@ -211,7 +211,10 @@ RSpec.describe SubscriptionsController do ...@@ -211,7 +211,10 @@ RSpec.describe SubscriptionsController do
it 'returns the selected group location in JSON format' do it 'returns the selected group location in JSON format' do
subject subject
expect(response.body).to eq({ location: "/#{selected_group.path}" }.to_json) plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
end end
context 'when selected group is a sub group' do context 'when selected group is a sub group' do
......
...@@ -348,7 +348,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do ...@@ -348,7 +348,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
expect(page).to have_text custom_stage_name expect(page).to have_text custom_stage_name
end end
it_behaves_like 'can edit custom stages' it_behaves_like 'can edit custom stages', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/273045'
end end
end end
......
...@@ -9223,6 +9223,15 @@ msgstr "" ...@@ -9223,6 +9223,15 @@ msgstr ""
msgid "DevOps Score" msgid "DevOps Score"
msgstr "" 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" msgid "Diff content limits"
msgstr "" msgstr ""
......
...@@ -23,6 +23,7 @@ Disallow: /users ...@@ -23,6 +23,7 @@ Disallow: /users
Disallow: /help Disallow: /help
Disallow: /s/ Disallow: /s/
Disallow: /-/profile Disallow: /-/profile
Disallow: /-/ide/
# Only specifically allow the Sign In page to avoid very ugly search results # Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in Allow: /users/sign_in
......
#!/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}."
#!/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}."
...@@ -48,6 +48,43 @@ function update_tests_metadata() { ...@@ -48,6 +48,43 @@ function update_tests_metadata() {
fi 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() { function rspec_simple_job() {
local rspec_opts="${1}" local rspec_opts="${1}"
......
#!/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}."
# 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
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);
});
});
});
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);
});
});
...@@ -370,6 +370,26 @@ RSpec.describe GroupsHelper do ...@@ -370,6 +370,26 @@ RSpec.describe GroupsHelper do
end end
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 describe '#show_invite_banner?' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) } let_it_be_with_refind(:group) { create(:group) }
......
...@@ -161,6 +161,12 @@ RSpec.describe BroadcastMessage do ...@@ -161,6 +161,12 @@ RSpec.describe BroadcastMessage do
expect(subject.call('/group/issues/test').length).to eq(1) expect(subject.call('/group/issues/test').length).to eq(1)
end 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 end
describe '.current', :use_clean_rails_memory_store_caching do describe '.current', :use_clean_rails_memory_store_caching do
......
...@@ -37,6 +37,7 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do ...@@ -37,6 +37,7 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
'/help', '/help',
'/s/', '/s/',
'/-/profile', '/-/profile',
'/-/ide/project',
'/foo/bar/new', '/foo/bar/new',
'/foo/bar/edit', '/foo/bar/edit',
'/foo/bar/raw', '/foo/bar/raw',
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
require './spec/simplecov_env' require './spec/simplecov_env'
SimpleCovEnv.start! SimpleCovEnv.start!
require './spec/crystalball_env'
CrystalballEnv.start!
ENV["RAILS_ENV"] = 'test' ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true' ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
......
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment