Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-28 15:08:49 +00:00
parent 24e7d18539
commit 77d49e6a73
103 changed files with 1329 additions and 125 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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/

View File

@ -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

View File

@ -1 +1 @@
bee8517ab043ff98c283a5f191e68e2bd75eb9de cf8e99ccc104f0a43f41e54896ee46a5e1b15a0a

View File

@ -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

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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'),
},
};

View File

@ -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);
},
});
};

View File

@ -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);
},
});
};

View File

@ -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];

View File

@ -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);
},
});
});

View File

@ -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;
} }

View File

@ -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);

View File

@ -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?

View File

@ -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('\\*', '.*')

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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'

View File

@ -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}" }

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Disallow WebIDE route in robots.txt
merge_request: 46117
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Don't return target-specific broadcasts without a current path supplied
merge_request: 46322
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add webhooks for feature flag
merge_request: 41863
author: Sashi
type: added

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
a9605126178d887bbf526a4a33b7060b072eff7a8d6712e3552099f7e615f88b

View File

@ -0,0 +1 @@
234711b96d3869fe826dfd71ae29e0f75e50302bc29a4e60f436ec76b4be3efb

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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.

View File

@ -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:**

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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
--- ---

View File

@ -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
--- ---

View File

@ -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
--- ---

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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 ""

View File

@ -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

19
scripts/generate-test-mapping Executable file
View File

@ -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}."

19
scripts/pack-test-mapping Executable file
View File

@ -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}."

View File

@ -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}"

17
scripts/unpack-test-mapping Executable file
View File

@ -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}."

25
spec/crystalball_env.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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);
});
});

View File

@ -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}"]`);

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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