Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
24e7d18539
commit
77d49e6a73
|
@ -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:
|
||||||
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 @@
|
||||||
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 @@
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
- coverage/
|
- coverage/
|
||||||
|
- crystalball/
|
||||||
- knapsack/
|
- knapsack/
|
||||||
- rspec_flaky/
|
- rspec_flaky/
|
||||||
- rspec_profiling/
|
- rspec_profiling/
|
||||||
|
|
|
@ -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:
|
||||||
- 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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
bee8517ab043ff98c283a5f191e68e2bd75eb9de
|
cf8e99ccc104f0a43f41e54896ee46a5e1b15a0a
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -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
|
||||||
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
|
||||||
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)
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'),
|
||||||
|
},
|
||||||
|
};
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -69,9 +69,12 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'incidentSla',
|
key: 'incidentSla',
|
||||||
label: s__('IncidentManagement|Time to SLA'),
|
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`,
|
tdClass: `${tdClass} gl-text-right`,
|
||||||
thAttr: TH_INCIDENT_SLA_TEST_ID,
|
thAttr: TH_INCIDENT_SLA_TEST_ID,
|
||||||
|
sortKey: 'SLA_DUE_AT',
|
||||||
|
sortable: true,
|
||||||
|
sortDirection: 'asc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'assignees',
|
key: 'assignees',
|
||||||
|
@ -253,13 +256,22 @@ export default {
|
||||||
this.redirecting = true;
|
this.redirecting = true;
|
||||||
},
|
},
|
||||||
fetchSortedData({ sortBy, sortDesc }) {
|
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 sortingDirection = sortDesc ? 'DESC' : 'ASC';
|
||||||
const sortingColumn = convertToSnakeCase(sortBy)
|
|
||||||
.replace(/_.*/, '')
|
// Use `sortKey` if provided, otherwise fall back to existing algorithm
|
||||||
.toUpperCase();
|
if (field?.sortKey) {
|
||||||
|
sortKey = field.sortKey;
|
||||||
|
} else {
|
||||||
|
sortKey = convertToSnakeCase(sortBy)
|
||||||
|
.replace(/_.*/, '')
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
this.pagination = initialPaginationState;
|
this.pagination = initialPaginationState;
|
||||||
this.sort = `${sortingColumn}_${sortingDirection}`;
|
this.sort = `${sortKey}_${sortingDirection}`;
|
||||||
},
|
},
|
||||||
getSeverity(severity) {
|
getSeverity(severity) {
|
||||||
return INCIDENT_SEVERITY[severity];
|
return INCIDENT_SEVERITY[severity];
|
||||||
|
|
|
@ -1,27 +1,5 @@
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -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-'] {
|
[id^='editor-lite-'] {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
@mixin spinner($size: 16px, $border-width: 2px, $color: $orange-400) {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -30,8 +30,12 @@
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@include spinner-size(16px, 2px);
|
@include spinner-size($size, $border-width);
|
||||||
@include spinner-color($orange-400);
|
@include spinner-color($color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
@include spinner;
|
||||||
|
|
||||||
&.spinner-md {
|
&.spinner-md {
|
||||||
@include spinner-size(32px, 3px);
|
@include spinner-size(32px, 3px);
|
||||||
|
|
|
@ -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
|
||||||
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('\\*', '.*')
|
||||||
|
|
|
@ -6,18 +6,25 @@
|
||||||
module IssueAvailableFeatures
|
module IssueAvailableFeatures
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
# EE only features are listed on EE::IssueAvailableFeatures
|
class_methods do
|
||||||
def available_features_for_issue_types
|
# EE only features are listed on EE::IssueAvailableFeatures
|
||||||
{}.with_indifferent_access
|
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
|
end
|
||||||
|
|
||||||
def issue_type_supports?(feature)
|
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'
|
raise ArgumentError, 'invalid feature'
|
||||||
end
|
end
|
||||||
|
|
||||||
available_features_for_issue_types[feature].include?(issue_type)
|
self.class.available_features_for_issue_types[feature].include?(issue_type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
|
IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
|
||||||
|
IssueAvailableFeatures::ClassMethods.prepend_if_ee('EE::IssueAvailableFeatures::ClassMethods')
|
||||||
|
|
|
@ -13,7 +13,8 @@ module TriggerableHooks
|
||||||
job_hooks: :job_events,
|
job_hooks: :job_events,
|
||||||
pipeline_hooks: :pipeline_events,
|
pipeline_hooks: :pipeline_events,
|
||||||
wiki_page_hooks: :wiki_page_events,
|
wiki_page_hooks: :wiki_page_events,
|
||||||
deployment_hooks: :deployment_events
|
deployment_hooks: :deployment_events,
|
||||||
|
feature_flag_hooks: :feature_flag_events
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
|
@ -18,7 +18,8 @@ class ProjectHook < WebHook
|
||||||
:job_hooks,
|
:job_hooks,
|
||||||
:pipeline_hooks,
|
:pipeline_hooks,
|
||||||
:wiki_page_hooks,
|
:wiki_page_hooks,
|
||||||
:deployment_hooks
|
:deployment_hooks,
|
||||||
|
:feature_flag_hooks
|
||||||
]
|
]
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
module Operations
|
module Operations
|
||||||
class FeatureFlag < ApplicationRecord
|
class FeatureFlag < ApplicationRecord
|
||||||
|
include AfterCommitQueue
|
||||||
include AtomicInternalId
|
include AtomicInternalId
|
||||||
include IidRoutes
|
include IidRoutes
|
||||||
include Limitable
|
include Limitable
|
||||||
|
@ -77,6 +78,22 @@ module Operations
|
||||||
Ability.issues_readable_by_user(issues, current_user)
|
Ability.issues_readable_by_user(issues, current_user)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def version_associations
|
def version_associations
|
||||||
|
|
|
@ -22,6 +22,10 @@ module FeatureFlags
|
||||||
|
|
||||||
audit_event = audit_event(feature_flag)
|
audit_event = audit_event(feature_flag)
|
||||||
|
|
||||||
|
if feature_flag.active_changed?
|
||||||
|
feature_flag.execute_hooks(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
if feature_flag.save
|
if feature_flag.save
|
||||||
save_audit_event(audit_event)
|
save_audit_event(audit_event)
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
- 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}" }
|
||||||
|
|
|
@ -78,6 +78,12 @@
|
||||||
%strong= s_('Webhooks|Deployment events')
|
%strong= s_('Webhooks|Deployment events')
|
||||||
%p.text-muted.ml-1
|
%p.text-muted.ml-1
|
||||||
= s_('Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled')
|
= 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-group
|
||||||
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
|
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
|
||||||
.form-check
|
.form-check
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Disallow WebIDE route in robots.txt
|
||||||
|
merge_request: 46117
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Don't return target-specific broadcasts without a current path supplied
|
||||||
|
merge_request: 46322
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add webhooks for feature flag
|
||||||
|
merge_request: 41863
|
||||||
|
author: Sashi
|
||||||
|
type: added
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
a9605126178d887bbf526a4a33b7060b072eff7a8d6712e3552099f7e615f88b
|
|
@ -0,0 +1 @@
|
||||||
|
234711b96d3869fe826dfd71ae29e0f75e50302bc29a4e60f436ec76b4be3efb
|
|
@ -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 (
|
CREATE TABLE project_compliance_framework_settings (
|
||||||
project_id bigint NOT NULL,
|
project_id bigint NOT NULL,
|
||||||
framework smallint NOT NULL,
|
framework smallint,
|
||||||
framework_id bigint,
|
framework_id bigint,
|
||||||
CONSTRAINT check_d348de9e2d CHECK ((framework_id IS NOT NULL))
|
CONSTRAINT check_d348de9e2d CHECK ((framework_id IS NOT NULL))
|
||||||
);
|
);
|
||||||
|
@ -17295,7 +17295,8 @@ CREATE TABLE web_hooks (
|
||||||
encrypted_token_iv character varying,
|
encrypted_token_iv character varying,
|
||||||
encrypted_url character varying,
|
encrypted_url character varying,
|
||||||
encrypted_url_iv 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
|
CREATE SEQUENCE web_hooks_id_seq
|
||||||
|
|
|
@ -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
|
# API Docs
|
||||||
|
|
||||||
Automate GitLab via a simple and powerful API.
|
Automate GitLab via a simple and powerful API.
|
||||||
|
|
|
@ -2803,7 +2803,7 @@ type ComplianceFramework {
|
||||||
"""
|
"""
|
||||||
Name of the compliance framework
|
Name of the compliance framework
|
||||||
"""
|
"""
|
||||||
name: ProjectSettingEnum!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -6908,6 +6908,11 @@ type EpicIssue implements CurrentUserTodos & Noteable {
|
||||||
"""
|
"""
|
||||||
blocked: Boolean!
|
blocked: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Count of issues blocking this issue
|
||||||
|
"""
|
||||||
|
blockedByCount: Int
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Timestamp of when the issue was closed
|
Timestamp of when the issue was closed
|
||||||
"""
|
"""
|
||||||
|
@ -9176,6 +9181,11 @@ type Issue implements CurrentUserTodos & Noteable {
|
||||||
"""
|
"""
|
||||||
blocked: Boolean!
|
blocked: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Count of issues blocking this issue
|
||||||
|
"""
|
||||||
|
blockedByCount: Int
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Timestamp of when the issue was closed
|
Timestamp of when the issue was closed
|
||||||
"""
|
"""
|
||||||
|
@ -10206,6 +10216,16 @@ enum IssueSort {
|
||||||
"""
|
"""
|
||||||
SEVERITY_DESC
|
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 at ascending order
|
||||||
"""
|
"""
|
||||||
|
@ -15701,17 +15721,6 @@ type ProjectPermissions {
|
||||||
uploadFile: Boolean!
|
uploadFile: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
|
||||||
Names of compliance frameworks that can be assigned to a Project
|
|
||||||
"""
|
|
||||||
enum ProjectSettingEnum {
|
|
||||||
gdpr
|
|
||||||
hipaa
|
|
||||||
pci_dss
|
|
||||||
soc_2
|
|
||||||
sox
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectStatistics {
|
type ProjectStatistics {
|
||||||
"""
|
"""
|
||||||
Build artifacts size of the project
|
Build artifacts size of the project
|
||||||
|
|
|
@ -7545,8 +7545,8 @@
|
||||||
"kind": "NON_NULL",
|
"kind": "NON_NULL",
|
||||||
"name": null,
|
"name": null,
|
||||||
"ofType": {
|
"ofType": {
|
||||||
"kind": "ENUM",
|
"kind": "SCALAR",
|
||||||
"name": "ProjectSettingEnum",
|
"name": "String",
|
||||||
"ofType": null
|
"ofType": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19041,6 +19041,20 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"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",
|
"name": "closedAt",
|
||||||
"description": "Timestamp of when the issue was closed",
|
"description": "Timestamp of when the issue was closed",
|
||||||
|
@ -24983,6 +24997,20 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"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",
|
"name": "closedAt",
|
||||||
"description": "Timestamp of when the issue was closed",
|
"description": "Timestamp of when the issue was closed",
|
||||||
|
@ -27868,6 +27896,18 @@
|
||||||
"description": "Published issues shown first",
|
"description": "Published issues shown first",
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"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
|
"possibleTypes": null
|
||||||
|
@ -45522,47 +45562,6 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"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",
|
"kind": "OBJECT",
|
||||||
"name": "ProjectStatistics",
|
"name": "ProjectStatistics",
|
||||||
|
|
|
@ -436,7 +436,7 @@ Represents a ComplianceFramework associated with a Project.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ----- | ---- | ----------- |
|
| ----- | ---- | ----------- |
|
||||||
| `name` | ProjectSettingEnum! | Name of the compliance framework |
|
| `name` | String! | Name of the compliance framework |
|
||||||
|
|
||||||
### ConfigureSastPayload
|
### ConfigureSastPayload
|
||||||
|
|
||||||
|
@ -1115,6 +1115,7 @@ Relationship between an epic and an issue.
|
||||||
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
|
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
|
||||||
| `author` | User! | User that created the issue |
|
| `author` | User! | User that created the issue |
|
||||||
| `blocked` | Boolean! | Indicates the issue is blocked |
|
| `blocked` | Boolean! | Indicates the issue is blocked |
|
||||||
|
| `blockedByCount` | Int | Count of issues blocking this issue |
|
||||||
| `closedAt` | Time | Timestamp of when the issue was closed |
|
| `closedAt` | Time | Timestamp of when the issue was closed |
|
||||||
| `confidential` | Boolean! | Indicates the issue is confidential |
|
| `confidential` | Boolean! | Indicates the issue is confidential |
|
||||||
| `createdAt` | Time! | Timestamp of when the issue was created |
|
| `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 |
|
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
|
||||||
| `author` | User! | User that created the issue |
|
| `author` | User! | User that created the issue |
|
||||||
| `blocked` | Boolean! | Indicates the issue is blocked |
|
| `blocked` | Boolean! | Indicates the issue is blocked |
|
||||||
|
| `blockedByCount` | Int | Count of issues blocking this issue |
|
||||||
| `closedAt` | Time | Timestamp of when the issue was closed |
|
| `closedAt` | Time | Timestamp of when the issue was closed |
|
||||||
| `confidential` | Boolean! | Indicates the issue is confidential |
|
| `confidential` | Boolean! | Indicates the issue is confidential |
|
||||||
| `createdAt` | Time! | Timestamp of when the issue was created |
|
| `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 |
|
| `RELATIVE_POSITION_ASC` | Relative position by ascending order |
|
||||||
| `SEVERITY_ASC` | Severity from less critical to more critical |
|
| `SEVERITY_ASC` | Severity from less critical to more critical |
|
||||||
| `SEVERITY_DESC` | Severity from more critical to less 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_ASC` | Updated at ascending order |
|
||||||
| `UPDATED_DESC` | Updated at descending order |
|
| `UPDATED_DESC` | Updated at descending order |
|
||||||
| `WEIGHT_ASC` | Weight by ascending order |
|
| `WEIGHT_ASC` | Weight by ascending order |
|
||||||
|
@ -3677,18 +3681,6 @@ Values for sorting projects.
|
||||||
| `SUCCESS` | |
|
| `SUCCESS` | |
|
||||||
| `WAITING_FOR_RESOURCE` | |
|
| `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
|
### RegistryState
|
||||||
|
|
||||||
State of a Geo registry.
|
State of a Geo registry.
|
||||||
|
|
|
@ -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
|
# 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
|
||||||
|
|
|
@ -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
|
# Groups API
|
||||||
|
|
||||||
## List groups
|
## List groups
|
||||||
|
|
|
@ -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 API
|
||||||
|
|
||||||
## Import repository from GitHub
|
## Import repository from GitHub
|
||||||
|
|
|
@ -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
|
# 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.
|
||||||
|
|
|
@ -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)**
|
# 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.
|
||||||
|
|
|
@ -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
|
# Issues Statistics API
|
||||||
|
|
||||||
Every API call to issues_statistics must be authenticated.
|
Every API call to issues_statistics must be authenticated.
|
||||||
|
|
|
@ -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
|
# Job Artifacts API
|
||||||
|
|
||||||
## Get job artifacts
|
## Get job artifacts
|
||||||
|
|
|
@ -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)**
|
# 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
|
||||||
|
|
|
@ -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)**
|
# Managed Licenses API **(ULTIMATE)**
|
||||||
|
|
||||||
## List managed licenses
|
## List managed licenses
|
||||||
|
|
|
@ -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
|
# Group and project members API
|
||||||
|
|
||||||
## Valid access levels
|
## Valid access levels
|
||||||
|
|
|
@ -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
|
# Namespaces API
|
||||||
|
|
||||||
Usernames and groupnames fall under a special category called namespaces.
|
Usernames and groupnames fall under a special category called namespaces.
|
||||||
|
|
|
@ -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 API
|
||||||
|
|
||||||
Notes are comments on:
|
Notes are comments on:
|
||||||
|
|
|
@ -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
|
# 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.
|
||||||
|
|
|
@ -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)**
|
# 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).
|
||||||
|
|
|
@ -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 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.
|
||||||
|
|
|
@ -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)**
|
# 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.
|
||||||
|
|
|
@ -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
|
# Services API
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
|
|
|
@ -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)**
|
# 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
|
||||||
|
|
|
@ -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)**
|
# Sidekiq Metrics API **(CORE ONLY)**
|
||||||
|
|
||||||
> Introduced in GitLab 8.9.
|
> Introduced in GitLab 8.9.
|
||||||
|
|
|
@ -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
|
# Application statistics API
|
||||||
|
|
||||||
## Get current application statistics
|
## Get current application statistics
|
||||||
|
|
|
@ -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
|
# System hooks API
|
||||||
|
|
||||||
All methods require administrator authorization.
|
All methods require administrator authorization.
|
||||||
|
|
|
@ -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
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
# Users API
|
||||||
|
|
||||||
## List users
|
## List users
|
||||||
|
|
|
@ -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
|
# 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.
|
||||||
|
|
|
@ -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
|
# 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)
|
||||||
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
|
||||||
|
|
|
@ -1358,6 +1358,55 @@ X-Gitlab-Event: Deployment Hook
|
||||||
|
|
||||||
Note that `deployable_id` is the ID of the CI job.
|
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
|
## Image URL rewriting
|
||||||
|
|
||||||
From GitLab 11.2, simple image references are rewritten to use an absolute URL
|
From GitLab 11.2, simple image references are rewritten to use an absolute URL
|
||||||
|
|
|
@ -8,6 +8,8 @@ module API
|
||||||
|
|
||||||
helpers ::API::Helpers::MembersHelpers
|
helpers ::API::Helpers::MembersHelpers
|
||||||
|
|
||||||
|
feature_category :authentication_and_authorization
|
||||||
|
|
||||||
%w[group project].each do |source_type|
|
%w[group project].each do |source_type|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: "The #{source_type} ID"
|
requires :id, type: String, desc: "The #{source_type} ID"
|
||||||
|
|
|
@ -8,6 +8,8 @@ module API
|
||||||
|
|
||||||
before { authenticated_as_admin! }
|
before { authenticated_as_admin! }
|
||||||
|
|
||||||
|
feature_category :continuous_integration
|
||||||
|
|
||||||
namespace 'admin' do
|
namespace 'admin' do
|
||||||
namespace 'ci' do
|
namespace 'ci' do
|
||||||
namespace 'variables' do
|
namespace 'variables' do
|
||||||
|
|
|
@ -5,6 +5,8 @@ module API
|
||||||
class InstanceClusters < ::API::Base
|
class InstanceClusters < ::API::Base
|
||||||
include PaginationParams
|
include PaginationParams
|
||||||
|
|
||||||
|
feature_category :kubernetes_management
|
||||||
|
|
||||||
before do
|
before do
|
||||||
authenticated_as_admin!
|
authenticated_as_admin!
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,8 @@ module API
|
||||||
class Sidekiq < ::API::Base
|
class Sidekiq < ::API::Base
|
||||||
before { authenticated_as_admin! }
|
before { authenticated_as_admin! }
|
||||||
|
|
||||||
|
feature_category :not_owned
|
||||||
|
|
||||||
namespace 'admin' do
|
namespace 'admin' do
|
||||||
namespace 'sidekiq' do
|
namespace 'sidekiq' do
|
||||||
namespace 'queues' do
|
namespace 'queues' do
|
||||||
|
|
|
@ -4,6 +4,8 @@ module API
|
||||||
class Appearance < ::API::Base
|
class Appearance < ::API::Base
|
||||||
before { authenticated_as_admin! }
|
before { authenticated_as_admin! }
|
||||||
|
|
||||||
|
feature_category :navigation
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
def current_appearance
|
def current_appearance
|
||||||
@current_appearance ||= (::Appearance.current || ::Appearance.new)
|
@current_appearance ||= (::Appearance.current || ::Appearance.new)
|
||||||
|
|
|
@ -5,6 +5,8 @@ module API
|
||||||
class Applications < ::API::Base
|
class Applications < ::API::Base
|
||||||
before { authenticated_as_admin! }
|
before { authenticated_as_admin! }
|
||||||
|
|
||||||
|
feature_category :authentication_and_authorization
|
||||||
|
|
||||||
resource :applications do
|
resource :applications do
|
||||||
helpers do
|
helpers do
|
||||||
def validate_redirect_uri(value)
|
def validate_redirect_uri(value)
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
module API
|
module API
|
||||||
class Avatar < ::API::Base
|
class Avatar < ::API::Base
|
||||||
|
feature_category :users
|
||||||
|
|
||||||
resource :avatar do
|
resource :avatar do
|
||||||
desc 'Return avatar url for a user' do
|
desc 'Return avatar url for a user' do
|
||||||
success Entities::Avatar
|
success Entities::Avatar
|
||||||
|
|
|
@ -6,9 +6,9 @@ module API
|
||||||
|
|
||||||
before { authenticate! }
|
before { authenticate! }
|
||||||
AWARDABLES = [
|
AWARDABLES = [
|
||||||
{ type: 'issue', find_by: :iid },
|
{ type: 'issue', find_by: :iid, feature_category: :issue_tracking },
|
||||||
{ type: 'merge_request', find_by: :iid },
|
{ type: 'merge_request', find_by: :iid, feature_category: :code_review },
|
||||||
{ type: 'snippet', find_by: :id }
|
{ type: 'snippet', find_by: :id, feature_category: :snippets }
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
params do
|
params do
|
||||||
|
@ -34,7 +34,7 @@ module API
|
||||||
params do
|
params do
|
||||||
use :pagination
|
use :pagination
|
||||||
end
|
end
|
||||||
get endpoint do
|
get endpoint, feature_category: awardable_params[:feature_category] do
|
||||||
if can_read_awardable?
|
if can_read_awardable?
|
||||||
awards = awardable.award_emoji
|
awards = awardable.award_emoji
|
||||||
present paginate(awards), with: Entities::AwardEmoji
|
present paginate(awards), with: Entities::AwardEmoji
|
||||||
|
@ -50,7 +50,7 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :award_id, type: Integer, desc: 'The ID of the award'
|
requires :award_id, type: Integer, desc: 'The ID of the award'
|
||||||
end
|
end
|
||||||
get "#{endpoint}/:award_id" do
|
get "#{endpoint}/:award_id", feature_category: awardable_params[:feature_category] do
|
||||||
if can_read_awardable?
|
if can_read_awardable?
|
||||||
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
|
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
|
||||||
else
|
else
|
||||||
|
@ -65,7 +65,7 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
|
requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
|
||||||
end
|
end
|
||||||
post endpoint do
|
post endpoint, feature_category: awardable_params[:feature_category] do
|
||||||
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
|
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
|
||||||
|
|
||||||
service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
|
service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
|
||||||
|
@ -84,7 +84,7 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :award_id, type: Integer, desc: 'The ID of an award emoji'
|
requires :award_id, type: Integer, desc: 'The ID of an award emoji'
|
||||||
end
|
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])
|
award = awardable.award_emoji.find(params[:award_id])
|
||||||
|
|
||||||
unauthorized! unless award.user == current_user || current_user.admin?
|
unauthorized! unless award.user == current_user || current_user.admin?
|
||||||
|
|
|
@ -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
|
|
@ -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 ""
|
||||||
|
|
||||||
|
@ -29704,6 +29713,9 @@ msgstr ""
|
||||||
msgid "Webhooks|Enable SSL verification"
|
msgid "Webhooks|Enable SSL verification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Webhooks|Feature Flag events"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Webhooks|Issues events"
|
msgid "Webhooks|Issues events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -29731,6 +29743,9 @@ msgstr ""
|
||||||
msgid "Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled"
|
msgid "Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled"
|
||||||
msgstr ""
|
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"
|
msgid "Webhooks|This URL will be triggered by a push to the repository"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}."
|
|
@ -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}."
|
|
@ -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}"
|
||||||
|
|
||||||
|
|
|
@ -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}."
|
|
@ -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
|
|
@ -22,6 +22,7 @@ FactoryBot.define do
|
||||||
pipeline_events { true }
|
pipeline_events { true }
|
||||||
wiki_page_events { true }
|
wiki_page_events { true }
|
||||||
deployment_events { true }
|
deployment_events { true }
|
||||||
|
feature_flag_events { true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TH_CREATED_AT_TEST_ID,
|
TH_CREATED_AT_TEST_ID,
|
||||||
TH_SEVERITY_TEST_ID,
|
TH_SEVERITY_TEST_ID,
|
||||||
TH_PUBLISHED_TEST_ID,
|
TH_PUBLISHED_TEST_ID,
|
||||||
|
TH_INCIDENT_SLA_TEST_ID,
|
||||||
trackIncidentCreateNewOptions,
|
trackIncidentCreateNewOptions,
|
||||||
trackIncidentListViewsOptions,
|
trackIncidentListViewsOptions,
|
||||||
} from '~/incidents/constants';
|
} from '~/incidents/constants';
|
||||||
|
@ -277,10 +278,11 @@ describe('Incidents List', () => {
|
||||||
const noneSort = 'none';
|
const noneSort = 'none';
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
selector | initialSort | firstSort | nextSort
|
selector | initialSort | firstSort | nextSort
|
||||||
${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
|
${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
|
||||||
${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
||||||
${TH_PUBLISHED_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 }) => {
|
`('updates sort with new direction', async ({ selector, initialSort, firstSort, nextSort }) => {
|
||||||
const [[attr, value]] = Object.entries(selector);
|
const [[attr, value]] = Object.entries(selector);
|
||||||
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
|
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -17,8 +17,14 @@ RSpec.describe 'Every API endpoint' do
|
||||||
let_it_be(:routes_without_category) do
|
let_it_be(:routes_without_category) do
|
||||||
api_endpoints.map do |(klass, path)|
|
api_endpoints.map do |(klass, path)|
|
||||||
next if klass.try(:feature_category_for_action, 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
|
# 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}"
|
"#{klass}##{path}"
|
||||||
end.compact.uniq
|
end.compact.uniq
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -261,4 +261,38 @@ RSpec.describe Operations::FeatureFlag do
|
||||||
expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id])
|
expect(flags.map(&:id)).to eq([feature_flag.id, feature_flag_b.id])
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -100,6 +100,13 @@ RSpec.describe FeatureFlags::UpdateService do
|
||||||
include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.')
|
include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.')
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when scope active state is changed' do
|
context 'when scope active state is changed' do
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue