Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-14 15:09:40 +00:00
parent 95671fac6e
commit fde3e0435c
67 changed files with 1012 additions and 235 deletions

View File

@ -276,6 +276,12 @@ GitlabSecurity/PublicSend:
Gitlab/DuplicateSpecLocation:
Enabled: true
Gitlab/PolicyRuleBoolean:
Enabled: true
Include:
- 'app/policies/**/*'
- 'ee/app/policies/**/*'
Cop/InjectEnterpriseEditionModule:
Enabled: true
Exclude:

View File

@ -41,6 +41,10 @@ Graphql/ResolverType:
- 'app/graphql/resolvers/users/group_count_resolver.rb'
- 'ee/app/graphql/resolvers/vulnerabilities_base_resolver.rb'
Gitlab/PolicyRuleBoolean:
Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb'
Rails/SaveBang:
Exclude:
- 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'

View File

@ -1,6 +1,18 @@
import { __ } from '~/locale';
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const reviewerToken = {
formattedKey: __('Reviewer'),
key: 'reviewer',
type: 'string',
param: 'username',
symbol: '@',
icon: 'user',
tag: '@reviewer',
};
IssuableTokenKeys.tokenKeys.splice(2, 0, reviewerToken);
IssuableTokenKeys.tokenKeysWithAlternative.splice(2, 0, reviewerToken);
const draftToken = {
token: {
formattedKey: __('Draft'),

View File

@ -21,15 +21,6 @@ export const tokenKeys = [
icon: 'user',
tag: '@assignee',
},
{
formattedKey: __('Reviewer'),
key: 'reviewer',
type: 'string',
param: 'username',
symbol: '@',
icon: 'user',
tag: '@reviewer',
},
{
formattedKey: __('Milestone'),
key: 'milestone',

View File

@ -0,0 +1,48 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
},
props: {
visibilityLevelOptions: {
type: Array,
required: true,
},
defaultLevel: {
type: Number,
required: true,
},
},
data() {
return {
selectedOption: this.getDefaultOption(),
};
},
methods: {
getDefaultOption() {
return this.visibilityLevelOptions.find(option => option.level === this.defaultLevel);
},
onClick(option) {
this.selectedOption = option;
},
},
};
</script>
<template>
<div>
<input type="hidden" name="group[visibility_level]" :value="selectedOption.level" />
<gl-dropdown :text="selectedOption.label" class="gl-w-full" menu-class="gl-w-full! gl-mb-0">
<gl-dropdown-item
v-for="option in visibilityLevelOptions"
:key="option.level"
:secondary-text="option.description"
@click="onClick(option)"
>
<div class="gl-font-weight-bold gl-mb-1">{{ option.label }}</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>

View File

@ -0,0 +1,24 @@
import Vue from 'vue';
import VisibilityLevelDropdown from './components/visibility_level_dropdown.vue';
export default () => {
const el = document.querySelector('.js-visibility-level-dropdown');
if (!el) {
return null;
}
const { visibilityLevelOptions, defaultLevel } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(VisibilityLevelDropdown, {
props: {
visibilityLevelOptions: JSON.parse(visibilityLevelOptions),
defaultLevel: Number(defaultLevel),
},
});
},
});
};

View File

@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query getProjectContainerRepositories(
query getGroupContainerRepositories(
$fullPath: ID!
$name: String
$first: Int

View File

@ -12,8 +12,8 @@ import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import {
ALERT_SUCCESS_TAG,

View File

@ -18,9 +18,9 @@ import RegistryHeader from '../components/list_page/registry_header.vue';
import ImageList from '../components/list_page/image_list.vue';
import CliCommands from '../components/list_page/cli_commands.vue';
import getProjectContainerRepositories from '../graphql/queries/get_project_container_repositories.graphql';
import getGroupContainerRepositories from '../graphql/queries/get_group_container_repositories.graphql';
import deleteContainerRepository from '../graphql/mutations/delete_container_repository.graphql';
import getProjectContainerRepositoriesQuery from '../graphql/queries/get_project_container_repositories.query.graphql';
import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql';
import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
@ -111,8 +111,8 @@ export default {
},
graphQlQuery() {
return this.config.isGroupPage
? getGroupContainerRepositories
: getProjectContainerRepositories;
? getGroupContainerRepositoriesQuery
: getProjectContainerRepositoriesQuery;
},
queryVariables() {
return {
@ -152,7 +152,7 @@ export default {
this.mutationLoading = true;
return this.$apollo
.mutate({
mutation: deleteContainerRepository,
mutation: deleteContainerRepositoryMutation,
variables: {
id: this.itemToDelete.id,
},

View File

@ -1,7 +1,7 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual, get, isEmpty } from 'lodash';
import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql';
import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import {
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,

View File

@ -20,7 +20,7 @@ import {
EXPIRATION_POLICY_FOOTER_NOTE,
} from '~/registry/settings/constants';
import { formOptionsGenerator } from '~/registry/settings/utils';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import ExpirationDropdown from './expiration_dropdown.vue';
import ExpirationInput from './expiration_input.vue';

View File

@ -1,5 +1,5 @@
import { produce } from 'immer';
import expirationPolicyQuery from '../queries/get_expiration_policy.graphql';
import expirationPolicyQuery from '../queries/get_expiration_policy.query.graphql';
export const updateContainerExpirationPolicy = projectPath => (client, { data: updatedData }) => {
const queryAndParams = {

View File

@ -0,0 +1,58 @@
<script>
import { GlButton, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlIcon,
GlLink,
GlPopover,
},
props: {
helpPath: {
type: String,
required: true,
},
discoverProjectSecurityPath: {
type: String,
required: false,
default: '',
},
},
i18n: {
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
upgradeToManageVulnerabilities: s__('SecurityReports|Upgrade to manage vulnerabilities'),
upgradeToInteract: s__(
'SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI.',
),
},
};
</script>
<template>
<span v-if="discoverProjectSecurityPath">
<gl-button
ref="discoverProjectSecurity"
icon="information-o"
category="tertiary"
:aria-label="$options.i18n.upgradeToManageVulnerabilities"
/>
<gl-popover
:target="() => $refs.discoverProjectSecurity.$el"
:title="$options.i18n.upgradeToManageVulnerabilities"
placement="top"
triggers="click blur"
>
{{ $options.i18n.upgradeToInteract }}
<gl-link :href="discoverProjectSecurityPath" target="_blank" class="gl-font-sm">{{
__('Learn more')
}}</gl-link>
</gl-popover>
</span>
<gl-link v-else target="_blank" :href="helpPath" :aria-label="$options.i18n.securityReportsHelp">
<gl-icon name="question" />
</gl-link>
</template>

View File

@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
@ -8,6 +8,7 @@ import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import Api from '~/api';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
import store from './store';
@ -23,10 +24,10 @@ import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
GlIcon,
GlLink,
GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
SecuritySummary,
},
@ -44,6 +45,11 @@ export default {
type: String,
required: true,
},
discoverProjectSecurityPath: {
type: String,
required: false,
default: '',
},
sastComparisonPath: {
type: String,
required: false,
@ -64,6 +70,11 @@ export default {
required: false,
default: 0,
},
canDiscoverProjectSecurity: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -231,7 +242,6 @@ export default {
downloadFromPipelineTab: s__(
'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
@ -248,14 +258,10 @@ export default {
<span :key="slot">
<security-summary :message="groupedSummaryText" />
<gl-link
target="_blank"
data-testid="help"
:href="securityReportsDocsPath"
:aria-label="$options.i18n.securityReportsHelp"
>
<gl-icon name="question" />
</gl-link>
<help-icon
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>
</span>
</template>
@ -300,14 +306,10 @@ export default {
</template>
</gl-sprintf>
<gl-link
target="_blank"
data-testid="help"
:href="securityReportsDocsPath"
:aria-label="$options.i18n.securityReportsHelp"
>
<gl-icon name="question" />
</gl-link>
<help-icon
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>
</template>
<template v-if="canShowDownloads" #action-buttons>

View File

@ -157,6 +157,16 @@ module VisibilityLevelHelper
end
end
def visibility_level_options(form_model)
available_visibility_levels(form_model).map do |level|
{
level: level,
label: visibility_level_label(level),
description: visibility_level_description(level, form_model)
}
end
end
def snippets_selected_visibility_level(visibility_levels, selected)
visibility_levels.find { |level| level == selected } || visibility_levels.min
end

View File

@ -12,10 +12,16 @@ module Timebox
include FromUnion
TimeboxStruct = Struct.new(:title, :name, :id) do
include GlobalID::Identification
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
end
def self.declarative_policy_class
"TimeboxPolicy"
end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
@ -24,8 +30,6 @@ module Timebox
Any = TimeboxStruct.new('Any Timebox', '', -1)
Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
Started = TimeboxStruct.new('Started', '#started', -3)
# For Iteration
Current = TimeboxStruct.new('Current', '#current', -4)
included do
# Defines the same constants above, but inside the including class.
@ -33,7 +37,6 @@ module Timebox
const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
const_set :Started, TimeboxStruct.new('Started', '#started', -3)
const_set :Current, TimeboxStruct.new('Current', '#current', -4)
alias_method :timebox_id, :id

View File

@ -9,6 +9,10 @@ class Milestone < ApplicationRecord
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
class Predefined
ALL = [::Timebox::None, ::Timebox::Any, ::Timebox::Started, ::Timebox::Upcoming].freeze
end
has_many :milestone_releases
has_many :releases, through: :milestone_releases

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class TimeboxPolicy < BasePolicy
# stub permissions policy on None, Any, Upcoming, Started and Current timeboxes
rule { default }.policy do
enable :read_iteration
enable :read_milestone
end
end

View File

@ -0,0 +1,3 @@
= f.label :visibility_level, class: 'label-bold' do
= _('Visibility level')
.js-visibility-level-dropdown{ data: { visibility_level_options: visibility_level_options(@group).to_json, default_level: f.object.visibility_level } }

View File

@ -13,6 +13,10 @@ class GitlabUsagePingWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_retry_in { |count| (count + 1) * 8.hours.to_i }
def perform
# Disable usage ping for GitLab.com
# See https://gitlab.com/gitlab-org/gitlab/-/issues/292929 for details
return if Gitlab.com?
# Multiple Sidekiq workers could run this. We should only do this at most once a day.
in_lock(LEASE_KEY, ttl: LEASE_TIMEOUT) do
# Splay the request over a minute to avoid thundering herd problems.

View File

@ -0,0 +1,5 @@
---
title: Show upgrade popover in security widget in merge requests when the user is able to upgrade
merge_request: 49613
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add ability to aggregated metrics in Usage Ping
merge_request: 49886
author:
type: added

View File

@ -1,8 +0,0 @@
---
name: product_analytics_aggregated_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44624
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267550
milestone: '13.6'
type: development
group: group::product analytics
default_enabled: false

View File

@ -1294,6 +1294,11 @@ type Board {
"""
id: ID!
"""
The board iteration.
"""
iteration: Iteration
"""
Labels of the board
"""
@ -23339,6 +23344,11 @@ input UpdateBoardInput {
"""
id: BoardID!
"""
The ID of iteration to be assigned to the board.
"""
iterationId: IterationID
"""
The IDs of labels to be added to the board
"""
@ -24901,6 +24911,11 @@ type Vulnerability implements Noteable {
last: Int
): VulnerabilityExternalIssueLinkConnection!
"""
Indicates whether there is a solution available for this vulnerability.
"""
hasSolutions: Boolean
"""
GraphQL ID of the vulnerability
"""

View File

@ -3449,6 +3449,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iteration",
"description": "The board iteration.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Iteration",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "labels",
"description": "Labels of the board",
@ -68291,6 +68305,16 @@
},
"defaultValue": null
},
{
"name": "iterationId",
"description": "The ID of iteration to be assigned to the board.",
"type": {
"kind": "SCALAR",
"name": "IterationID",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "The weight value to be assigned to the board",
@ -72465,6 +72489,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasSolutions",
"description": "Indicates whether there is a solution available for this vulnerability.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "GraphQL ID of the vulnerability",

View File

@ -247,6 +247,7 @@ Represents a project or group board.
| `hideBacklogList` | Boolean | Whether or not backlog list is hidden |
| `hideClosedList` | Boolean | Whether or not closed list is hidden |
| `id` | ID! | ID (global ID) of the board |
| `iteration` | Iteration | The board iteration. |
| `labels` | LabelConnection | Labels of the board |
| `lists` | BoardListConnection | Lists of the board |
| `milestone` | Milestone | The board milestone |
@ -3747,6 +3748,7 @@ Represents a vulnerability.
| `discussions` | DiscussionConnection! | All discussions on this noteable |
| `dismissedAt` | Time | Timestamp of when the vulnerability state was changed to dismissed |
| `externalIssueLinks` | VulnerabilityExternalIssueLinkConnection! | List of external issue links related to the vulnerability |
| `hasSolutions` | Boolean | Indicates whether there is a solution available for this vulnerability. |
| `id` | ID! | GraphQL ID of the vulnerability |
| `identifiers` | VulnerabilityIdentifier! => Array | Identifiers of the vulnerability. |
| `issueLinks` | VulnerabilityIssueLinkConnection! | List of issue links related to the vulnerability |

View File

@ -526,21 +526,28 @@ You can use the following fake tokens as examples:
### Usage list
<!-- vale off -->
| Usage | Guidance | [Vale](../testing.md#vale) Tests |
|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| currently | Do not use when talking about the product or its features. The documentation describes the product as it is today. | None |
| e.g., i.e., via | Do not use Latin abbreviations.<br><br>- Instead of **e.g.**, use **for example**, **such as**, **for instance**, or **like**.<br>- Instead of **i.e.**, use **that is**.<br>- Instead of **via**, use **with**, **through**, or **by using**. | [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml) |
| future tense | When possible, use present tense instead. For example, use `after you execute this command, GitLab displays the result` instead of `after you execute this command, GitLab will display the result`. | [`FutureTense.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FutureTense.yml) |
| high availability, HA | Do not use. Direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) for information about configuring GitLab to have the performance needed for additional users over time. | None |
| I, me | Do not use first-person singular. Use **you**, **we**, or **us** instead. | [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml) |
| jargon | Do not use. Define the term or [link to a definition](#links-to-external-documentation). | None |
| may, might | **Might** means something has the probability of occurring. **May** gives permission to do something. Consider **can** instead of **may**. | None |
| please | Do not use. For details, see the [Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/p/please). | None |
| profanity | Do not use. Doing so may negatively affect other users and contributors, which is contrary to the GitLab value of [Diversity, Inclusion, and Belonging](https://about.gitlab.com/handbook/values/#diversity-inclusion). | None |
| scalability | Do not use when talking about increasing GitLab performance for additional users. The words scale or scaling are sometimes acceptable, but references to increasing GitLab performance for additional users should direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) page. | None |
| simply, easily, handy, useful | Do not use. If the user doesn't find the process to be these things, we lose their trust. | None |
| slashes | Instead of **and/or** use **or** or another sensible construction. This rule applies to other slashes as well, like **follow/unfollow**. Exception like **CI/CD** are allowed. | None |
| that | Do not use. Example: `the file that you save` can be `the file you save`. | None |
| {::nomarkdown}<div style="width:140px">Usage</div>{:/} | Guidance |
|-----------------------|-----|
| and/or | Use **or** instead, or another sensible construction. |
| currently | Do not use when talking about the product or its features. The documentation describes the product as it is today. |
| easily | Do not use. If the user doesn't find the process to be these things, we lose their trust. |
| e.g. | Do not use Latin abbreviations. Use **for example**, **such as**, **for instance**, or **like** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml)) |
| future tense | When possible, use present tense instead. For example, use `after you execute this command, GitLab displays the result` instead of `after you execute this command, GitLab will display the result`. ([Vale](../testing.md#vale) rule: [`FutureTense.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FutureTense.yml)) |
| handy | Do not use. If the user doesn't find the process to be these things, we lose their trust. |
| high availability, HA | Do not use. Instead, direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) for information about configuring GitLab for handling greater amounts of users. |
| I | Do not use first-person singular. Use **you**, **we**, or **us** instead. ([Vale](../testing.md#vale) rule: [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml)) |
| i.e. | Do not use Latin abbreviations. Use **that is** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml)) |
| jargon | Do not use. Define the term or [link to a definition](#links-to-external-documentation). |
| may, might | **Might** means something has the probability of occurring. **May** gives permission to do something. Consider **can** instead of **may**. |
| me | Do not use first-person singular. Use **you**, **we**, or **us** instead. ([Vale](../testing.md#vale) rule: [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml)) |
| please | Do not use. For details, see the [Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/p/please). |
| profanity | Do not use. Doing so may negatively affect other users and contributors, which is contrary to the GitLab value of [Diversity, Inclusion, and Belonging](https://about.gitlab.com/handbook/values/#diversity-inclusion). |
| scalability | Do not use when talking about increasing GitLab performance for additional users. The words scale or scaling are sometimes acceptable, but references to increasing GitLab performance for additional users should direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) page. |
| simply | Do not use. If the user doesn't find the process to be these things, we lose their trust. |
| slashes | Instead of **and/or**, use **or** or another sensible construction. This rule also applies to other slashes, like **follow/unfollow**. Some exceptions (like **CI/CD**) are allowed. |
| that | Do not use. Example: `the file that you save` can be `the file you save`. |
| useful | Do not use. If the user doesn't find the process to be these things, we lose their trust. |
| via | Do not use Latin abbreviations. Use **with**, **through**, or **by using** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml)) |
<!-- vale on -->
### Contractions

View File

@ -178,6 +178,29 @@ if Feature.disabled?(:my_feature_flag, project, default_enabled: true)
end
```
If not specified, `default_enabled` is `false`.
To force reading the `default_enabled` value from the relative YAML definition file, use
`default_enabled: :yaml`:
```ruby
if Feature.enabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is enabled
end
```
```ruby
if Feature.disabled?(:feature_flag, project, default_enabled: :yaml)
# execute code if feature flag is disabled
end
```
This allows to use the same feature flag check across various parts of the codebase and
maintain the status of `default_enabled` in the YAML definition file which is the SSOT.
If `default_enabled: :yaml` is used, a YAML definition is expected or an error is raised
in development or test environment, while returning `false` on production.
If not specified, the default feature flag type for `Feature.enabled?` and `Feature.disabled?`
is `type: development`. For all other feature flag types, you must specify the `type:`:

View File

@ -63,10 +63,20 @@ end
Within the rule DSL, you can use:
- A regular word mentions a condition by name - a rule that is in effect when that condition is truthy.
- `~` indicates negation.
- `~` indicates negation, also available as `negate`.
- `&` and `|` are logical combinations, also available as `all?(...)` and `any?(...)`.
- `can?(:other_ability)` delegates to the rules that apply to `:other_ability`. Note that this is distinct from the instance method `can?`, which can check dynamically - this only configures a delegation to another ability.
`~`, `&` and `|` operators are overridden methods in
[`DeclarativePolicy::Rule::Base`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/declarative_policy/rule.rb).
Do not use boolean operators such as `&&` and `||` within the rule DSL,
as conditions within rule blocks are objects, not booleans. The same
applies for ternary operators (`condition ? ... : ...`), and `if`
blocks. These operators cannot be overridden, and are hence banned via a
[custom
cop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49771).
## Scores, Order, Performance
To see how the rules get evaluated into a judgment, it is useful in a console to use `policy.debug(:some_ability)`. This prints the rules in the order they are evaluated.

View File

@ -25,19 +25,6 @@ want to have more specific search results.
Advanced Search only supports searching the [default branch](../project/repository/branches/index.md#default-branch).
## Use cases
Let's say for example that the product you develop relies on the code of another
product that's hosted under some other group.
Since under your GitLab instance there are hosted hundreds of different projects,
you need the search results to be as efficient as possible. You have a feeling
of what you want to find (e.g., a function name), but at the same you're also
not so sure.
In that case, using the advanced search syntax in your query will yield much
better results.
## Using the Advanced Search Syntax
The Advanced Search Syntax supports fuzzy or exact search queries with prefixes,
@ -93,3 +80,10 @@ Examples:
- Finding `success` in all files excluding `.po|pot` files: [`success -filename:*.po*`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=success+-filename%3A*.po*&group_id=9970&project_id=278964)
- Finding `import` excluding minified JavaScript (`.min.js`) files: [`import -extension:min.js`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=import+-extension%3Amin.js&group_id=9970&project_id=278964)
- Finding `docs` for all files outside the `docs/` folder: [`docs -path:docs/`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=docs+-path%3Adocs%2F&group_id=9970&project_id=278964)
### Search by issue or merge request ID
You can search a specific issue or merge request by its ID with a special prefix.
- To search by issue ID, use prefix `#` followed by issue ID. For example, [#23456](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=%2323456&group_id=9970&project_id=278964)
- To search by merge request ID, use prefix `!` followed by merge request ID. For example [!23456](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=merge_requests&repository_ref=&search=%2123456&group_id=9970&project_id=278964)

View File

@ -68,6 +68,9 @@ class Feature
Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled)
end
# If `default_enabled: :yaml` we fetch the value from the YAML definition instead.
default_enabled = Feature::Definition.default_enabled?(key) if default_enabled == :yaml
# During setup the database does not exist yet. So we haven't stored a value
# for the feature yet and return the default.
return default_enabled unless Gitlab::Database.exists?

View File

@ -71,9 +71,7 @@ class Feature
"a valid syntax: #{TYPES.dig(type, :example)}"
end
# We accept an array of defaults as some features are undefined
# and have `default_enabled: true/false`
unless Array(default_enabled).include?(default_enabled_in_code)
unless default_enabled_in_code == :yaml || default_enabled == default_enabled_in_code
# Raise exception in test and dev
raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \
"#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}"
@ -96,6 +94,10 @@ class Feature
@definitions ||= load_all!
end
def get(key)
definitions[key.to_sym]
end
def reload!
@definitions = load_all!
end
@ -105,7 +107,7 @@ class Feature
end
def valid_usage!(key, type:, default_enabled:)
if definition = definitions[key.to_sym]
if definition = get(key)
definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled)
elsif type_definition = self::TYPES[type]
raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional]
@ -114,6 +116,17 @@ class Feature
end
end
def default_enabled?(key)
if definition = get(key)
definition.default_enabled
else
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
InvalidFeatureFlagError.new("The feature flag YAML definition for '#{key}' does not exist"))
false
end
end
def register_hot_reloader!
# Reload feature flags on change of this file or any `.yml`
file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do

View File

@ -12,7 +12,7 @@ module Gitlab
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update, :bridge, :content, :dry_run,
# These attributes are set by Chains during processing:
:config_content, :yaml_processor_result, :stage_seeds
:config_content, :yaml_processor_result, :pipeline_seed
) do
include Gitlab::Utils::StrongMemoize

View File

@ -10,12 +10,12 @@ module Gitlab
PopulateError = Class.new(StandardError)
def perform!
raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds
raise ArgumentError, 'missing pipeline seed' unless @command.pipeline_seed
##
# Populate pipeline with all stages, and stages with builds.
#
pipeline.stages = @command.stage_seeds.map(&:to_resource)
pipeline.stages = @command.pipeline_seed.stages
if stage_names.empty?
return error('No stages / jobs for this pipeline.')

View File

@ -29,11 +29,11 @@ module Gitlab
##
# Gather all runtime build/stage errors
#
if stage_seeds_errors
return error(stage_seeds_errors.join("\n"), config_error: true)
if pipeline_seed.errors
return error(pipeline_seed.errors.join("\n"), config_error: true)
end
@command.stage_seeds = stage_seeds
@command.pipeline_seed = pipeline_seed
end
def break?
@ -42,24 +42,12 @@ module Gitlab
private
def stage_seeds_errors
stage_seeds.flat_map(&:errors).compact.presence
end
def stage_seeds
strong_memoize(:stage_seeds) do
seeds = stages_attributes.inject([]) do |previous_stages, attributes|
seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages)
previous_stages + [seed]
end
seeds.select(&:included?)
def pipeline_seed
strong_memoize(:pipeline_seed) do
stages_attributes = @command.yaml_processor_result.stages_attributes
Gitlab::Ci::Pipeline::Seed::Pipeline.new(pipeline, stages_attributes)
end
end
def stages_attributes
@command.yaml_processor_result.stages_attributes
end
end
end
end

View File

@ -34,11 +34,7 @@ module Gitlab
def pipeline_deployment_count
strong_memoize(:pipeline_deployment_count) do
@command.stage_seeds.sum do |stage_seed|
stage_seed.seeds.count do |build_seed|
build_seed.attributes[:environment].present?
end
end
@command.pipeline_seed.deployments_count
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Seed
class Pipeline
include Gitlab::Utils::StrongMemoize
def initialize(pipeline, stages_attributes)
@pipeline = pipeline
@stages_attributes = stages_attributes
end
def errors
stage_seeds.flat_map(&:errors).compact.presence
end
def stages
stage_seeds.map(&:to_resource)
end
def size
stage_seeds.sum(&:size)
end
def deployments_count
stage_seeds.sum do |stage_seed|
stage_seed.seeds.count do |build_seed|
build_seed.attributes[:environment].present?
end
end
end
private
def stage_seeds
strong_memoize(:stage_seeds) do
seeds = @stages_attributes.inject([]) do |previous_stages, attributes|
seed = Gitlab::Ci::Pipeline::Seed::Stage.new(@pipeline, attributes, previous_stages)
previous_stages + [seed]
end
seeds.select(&:included?)
end
end
end
end
end
end
end

View File

@ -689,16 +689,12 @@ module Gitlab
end
def aggregated_metrics_monthly
return {} unless Feature.enabled?(:product_analytics_aggregated_metrics)
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_monthly_data
}
end
def aggregated_metrics_weekly
return {} unless Feature.enabled?(:product_analytics_aggregated_metrics)
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_weekly_data
}

View File

@ -11,7 +11,6 @@
- name: product_analytics_test_metrics_union
operator: OR
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']
feature_flag: product_analytics_aggregated_metrics
- name: product_analytics_test_metrics_intersection
operator: AND
events: ['i_search_total', 'i_search_advanced', 'i_search_paid']

View File

@ -24634,6 +24634,12 @@ msgstr ""
msgid "SecurityReports|Undo dismiss"
msgstr ""
msgid "SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI."
msgstr ""
msgid "SecurityReports|Upgrade to manage vulnerabilities"
msgstr ""
msgid "SecurityReports|Vulnerability Report"
msgstr ""

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Gitlab
# This cop checks for usage of boolean operators in rule blocks, which
# does not work because conditions are objects, not booleans.
#
# @example
#
# # bad, `conducts_electricity` returns a Rule object, not a boolean!
# rule { conducts_electricity && batteries }.enable :light_bulb
#
# # good
# rule { conducts_electricity & batteries }.enable :light_bulb
#
# @example
#
# # bad, `conducts_electricity` returns a Rule object, so the ternary is always going to be true
# rule { conducts_electricity ? can?(:magnetize) : batteries }.enable :motor
#
# # good
# rule { conducts_electricity & can?(:magnetize) }.enable :motor
# rule { ~conducts_electricity & batteries }.enable :motor
class PolicyRuleBoolean < RuboCop::Cop::Cop
def_node_search :has_and_operator?, <<~PATTERN
(and ...)
PATTERN
def_node_search :has_or_operator?, <<~PATTERN
(or ...)
PATTERN
def_node_search :has_if?, <<~PATTERN
(if ...)
PATTERN
def on_block(node)
return unless node.method_name == :rule
if has_and_operator?(node)
add_offense(node, message: '&& is not allowed within a rule block. Did you mean to use `&`?')
end
if has_or_operator?(node)
add_offense(node, message: '|| is not allowed within a rule block. Did you mean to use `|`?')
end
if has_if?(node)
add_offense(node, message: 'if and ternary operators are not allowed within a rule block.')
end
end
end
end
end
end

View File

@ -153,6 +153,14 @@ RSpec.describe 'Filter issues', :js do
end
end
describe 'filter by reviewer' do
it 'does not allow filtering by reviewer' do
find('.filtered-search').click
expect(page).not_to have_button('Reviewer')
end
end
describe 'filter issues by label' do
context 'only label' do
it 'filters issues by searched label' do

View File

@ -0,0 +1,73 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Component from '~/groups/components/visibility_level_dropdown.vue';
describe('Visibility Level Dropdown', () => {
let wrapper;
const options = [
{ level: 0, label: 'Private', description: 'Private description' },
{ level: 20, label: 'Public', description: 'Public description' },
];
const defaultLevel = 0;
const createComponent = propsData => {
wrapper = shallowMount(Component, {
propsData,
});
};
beforeEach(() => {
createComponent({
visibilityLevelOptions: options,
defaultLevel,
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const hiddenInputValue = () =>
wrapper.find("input[name='group[visibility_level]']").attributes('value');
const dropdownText = () => wrapper.find(GlDropdown).props('text');
const findDropdownItems = () =>
wrapper.findAll(GlDropdownItem).wrappers.map(option => ({
text: option.text(),
secondaryText: option.props('secondaryText'),
}));
describe('Default values', () => {
it('sets the value of the hidden input to the default value', () => {
expect(hiddenInputValue()).toBe(options[0].level.toString());
});
it('sets the text of the dropdown to the default value', () => {
expect(dropdownText()).toBe(options[0].label);
});
it('shows all dropdown options', () => {
expect(findDropdownItems()).toEqual(
options.map(({ label, description }) => ({ text: label, secondaryText: description })),
);
});
});
describe('Selecting an option', () => {
beforeEach(() => {
wrapper
.findAll(GlDropdownItem)
.at(1)
.vm.$emit('click');
});
it('sets the value of the hidden input to the selected value', () => {
expect(hiddenInputValue()).toBe(options[1].level.toString());
});
it('sets the text of the dropdown to the selected value', () => {
expect(dropdownText()).toBe(options[1].label);
});
});
});

View File

@ -12,8 +12,8 @@ import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.graphql';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import {
graphQLImageDetailsMock,

View File

@ -19,9 +19,9 @@ import {
SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants';
import getProjectContainerRepositories from '~/registry/explorer/graphql/queries/get_project_container_repositories.graphql';
import getGroupContainerRepositories from '~/registry/explorer/graphql/queries/get_group_container_repositories.graphql';
import deleteContainerRepository from '~/registry/explorer/graphql/mutations/delete_container_repository.graphql';
import getProjectContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql';
import getGroupContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql';
import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
import {
graphQLImageListMock,
@ -72,9 +72,9 @@ describe('List Page', () => {
localVue.use(VueApollo);
const requestHandlers = [
[getProjectContainerRepositories, resolver],
[getGroupContainerRepositories, groupResolver],
[deleteContainerRepository, mutationResolver],
[getProjectContainerRepositoriesQuery, resolver],
[getGroupContainerRepositoriesQuery, groupResolver],
[deleteContainerRepositoryMutation, mutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import component from '~/registry/settings/components/registry_settings_app.vue';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from '~/registry/settings/components/settings_form.vue';
import {
FETCH_SETTINGS_ERROR_MESSAGE,

View File

@ -4,8 +4,8 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,

View File

@ -1,5 +1,5 @@
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
describe('Registry settings cache update', () => {
let client;

View File

@ -0,0 +1,68 @@
import { GlLink, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
const helpPath = '/docs';
const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
describe('HelpIcon component', () => {
let wrapper;
const createWrapper = props => {
wrapper = shallowMount(HelpIcon, {
propsData: {
helpPath,
...props,
},
});
};
const findLink = () => wrapper.find(GlLink);
const findPopover = () => wrapper.find(GlPopover);
const findPopoverTarget = () => wrapper.find({ ref: 'discoverProjectSecurity' });
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('given a help path only', () => {
beforeEach(() => {
createWrapper();
});
it('does not render a popover', () => {
expect(findPopover().exists()).toBe(false);
});
it('renders a help link', () => {
expect(findLink().attributes()).toMatchObject({
href: helpPath,
target: '_blank',
});
});
});
describe('given a help path and discover project security path', () => {
beforeEach(() => {
createWrapper({ discoverProjectSecurityPath });
});
it('renders a popover', () => {
const popover = findPopover();
expect(popover.props('target')()).toBe(findPopoverTarget().element);
expect(popover.attributes()).toMatchObject({
title: HelpIcon.i18n.upgradeToManageVulnerabilities,
triggers: 'click blur',
});
expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
});
it('renders a link to the discover path', () => {
expect(findLink().attributes()).toMatchObject({
href: discoverProjectSecurityPath,
target: '_blank',
});
});
});
});

View File

@ -19,6 +19,7 @@ import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
@ -38,6 +39,7 @@ describe('Security reports app', () => {
pipelineId: 123,
projectId: 456,
securityReportsDocsPath: '/docs',
discoverProjectSecurityPath: '/discoverProjectSecurityPath',
};
const createComponent = options => {
@ -47,6 +49,9 @@ describe('Security reports app', () => {
{
localVue,
propsData: { ...props },
stubs: {
HelpIcon: true,
},
},
options,
),
@ -68,7 +73,7 @@ describe('Security reports app', () => {
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon);
const setupMockJobArtifact = reportType => {
jest
.spyOn(Api, 'pipelineJobs')
@ -133,8 +138,9 @@ describe('Security reports app', () => {
});
it('renders a help link', () => {
expect(findHelpLink().attributes()).toMatchObject({
href: props.securityReportsDocsPath,
expect(findHelpIconComponent().props()).toEqual({
helpPath: props.securityReportsDocsPath,
discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
});
});

View File

@ -284,4 +284,34 @@ RSpec.describe VisibilityLevelHelper do
it { is_expected.to eq(expected) }
end
end
describe '#visibility_level_options' do
let(:user) { build(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'returns the desired mapping' do
expected_options = [
{
level: 0,
label: 'Private',
description: 'The group and its projects can only be viewed by members.'
},
{
level: 10,
label: 'Internal',
description: 'The group and any internal projects can be viewed by any logged in user except external users.'
},
{
level: 20,
label: 'Public',
description: 'The group and any public projects can be viewed without any authentication.'
}
]
expect(helper.visibility_level_options(group)).to eq expected_options
end
end
end

View File

@ -64,6 +64,11 @@ RSpec.describe Feature::Definition do
expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) }
.to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/)
end
it 'allows passing `default_enabled: :yaml`' do
expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: :yaml) }
.not_to raise_error
end
end
end
@ -209,4 +214,58 @@ RSpec.describe Feature::Definition do
end
end
end
describe '.defaul_enabled?' do
subject { described_class.default_enabled?(key) }
context 'when feature flag exist' do
let(:key) { definition.key }
before do
allow(described_class).to receive(:definitions) do
{ definition.key => definition }
end
end
context 'when default_enabled is true' do
it 'returns the value from the definition' do
expect(subject).to eq(true)
end
end
context 'when default_enabled is false' do
let(:attributes) do
{ name: 'feature_flag',
type: 'development',
default_enabled: false }
end
it 'returns the value from the definition' do
expect(subject).to eq(false)
end
end
end
context 'when feature flag does not exist' do
let(:key) { :unknown_feature_flag }
context 'when on dev or test environment' do
it 'raises an error' do
expect { subject }.to raise_error(
Feature::InvalidFeatureFlagError,
"The feature flag YAML definition for 'unknown_feature_flag' does not exist")
end
end
context 'when on production environment' do
before do
allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
end
it 'returns false' do
expect(subject).to eq(false)
end
end
end
end
end

View File

@ -249,10 +249,12 @@ RSpec.describe Feature, stub_feature_flags: false do
Feature::Definition.new('development/my_feature_flag.yml',
name: 'my_feature_flag',
type: 'development',
default_enabled: false
default_enabled: default_enabled
).tap(&:validate!)
end
let(:default_enabled) { false }
before do
stub_env('LAZILY_CREATE_FEATURE_FLAG', '0')
@ -275,6 +277,63 @@ RSpec.describe Feature, stub_feature_flags: false do
expect { described_class.enabled?(:my_feature_flag, default_enabled: true) }
.to raise_error(/The `default_enabled:` of/)
end
context 'when `default_enabled: :yaml` is used in code' do
it 'reads the default from the YAML definition' do
expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(false)
end
context 'when default_enabled is true in the YAML definition' do
let(:default_enabled) { true }
it 'reads the default from the YAML definition' do
expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(true)
end
end
context 'when YAML definition does not exist for an optional type' do
let(:optional_type) { described_class::Shared::TYPES.find { |name, attrs| attrs[:optional] }.first }
context 'when in dev or test environment' do
it 'raises an error for dev' do
expect { described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml) }
.to raise_error(
Feature::InvalidFeatureFlagError,
"The feature flag YAML definition for 'non_existent_flag' does not exist")
end
end
context 'when in production' do
before do
allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
end
context 'when database exists' do
before do
allow(Gitlab::Database).to receive(:exists?).and_return(true)
end
it 'checks the persisted status and returns false' do
expect(described_class).to receive(:get).with(:non_existent_flag).and_call_original
expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
end
end
context 'when database does not exist' do
before do
allow(Gitlab::Database).to receive(:exists?).and_return(false)
end
it 'returns false without checking the status in the database' do
expect(described_class).not_to receive(:get)
expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false)
end
end
end
end
end
end
end

View File

@ -7,26 +7,13 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
let_it_be(:project, reload: true) { create(:project, namespace: namespace) }
let_it_be(:plan_limits, reload: true) { create(:plan_limits, :default_plan) }
let(:stage_seeds) do
[
double(:test, seeds: [
double(:test, attributes: {})
]),
double(:staging, seeds: [
double(:staging, attributes: { environment: 'staging' })
]),
double(:production, seeds: [
double(:production, attributes: { environment: 'production' })
])
]
end
let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2) }
let(:save_incompleted) { false }
let(:command) do
double(:command,
project: project,
stage_seeds: stage_seeds,
pipeline_seed: pipeline_seed,
save_incompleted: save_incompleted
)
end

View File

@ -50,8 +50,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'sets the seeds in the command object' do
run_chain
expect(command.stage_seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
expect(command.stage_seeds.count).to eq 1
expect(command.pipeline_seed).to be_a(Gitlab::Ci::Pipeline::Seed::Pipeline)
expect(command.pipeline_seed.size).to eq 1
end
context 'when no ref policy is specified' do
@ -63,16 +63,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
it 'correctly fabricates a stage seeds object' do
it 'correctly fabricates stages and builds' do
run_chain
seeds = command.stage_seeds
expect(seeds.size).to eq 2
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.second.attributes[:name]).to eq 'deploy'
expect(seeds.dig(0, 0, :name)).to eq 'rspec'
expect(seeds.dig(0, 1, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
seed = command.pipeline_seed
expect(seed.stages.size).to eq 2
expect(seed.size).to eq 3
expect(seed.stages.first.name).to eq 'test'
expect(seed.stages.second.name).to eq 'deploy'
expect(seed.stages[0].statuses[0].name).to eq 'rspec'
expect(seed.stages[0].statuses[1].name).to eq 'spinach'
expect(seed.stages[1].statuses[0].name).to eq 'production'
end
end
@ -88,14 +90,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
it 'returns stage seeds only assigned to master' do
it 'returns pipeline seed with jobs only assigned to master' do
run_chain
seeds = command.stage_seeds
seed = command.pipeline_seed
expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seed.size).to eq 1
expect(seed.stages.first.name).to eq 'test'
expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
@ -109,14 +111,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
}
end
it 'returns stage seeds only assigned to schedules' do
it 'returns pipeline seed with jobs only assigned to schedules' do
run_chain
seeds = command.stage_seeds
seed = command.pipeline_seed
expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seed.size).to eq 1
expect(seed.stages.first.name).to eq 'test'
expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
@ -141,11 +143,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'returns seeds for kubernetes dependent job' do
run_chain
seeds = command.stage_seeds
seed = command.pipeline_seed
expect(seeds.size).to eq 2
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
expect(seed.size).to eq 2
expect(seed.stages[0].statuses[0].name).to eq 'spinach'
expect(seed.stages[1].statuses[0].name).to eq 'production'
end
end
end
@ -154,10 +156,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'does not return seeds for kubernetes dependent job' do
run_chain
seeds = command.stage_seeds
seed = command.pipeline_seed
expect(seeds.size).to eq 1
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seed.size).to eq 1
expect(seed.stages[0].statuses[0].name).to eq 'spinach'
end
end
end
@ -173,10 +175,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
it 'returns stage seeds only when variables expression is truthy' do
run_chain
seeds = command.stage_seeds
seed = command.pipeline_seed
expect(seeds.size).to eq 1
expect(seeds.dig(0, 0, :name)).to eq 'unit'
expect(seed.size).to eq 1
expect(seed.stages[0].statuses[0].name).to eq 'unit'
end
end

View File

@ -10,24 +10,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do
let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
let(:stage_seeds) do
[
double(:test, seeds: [
double(:test, attributes: {})
]),
double(:staging, seeds: [
double(:staging, attributes: { environment: 'staging' })
]),
double(:production, seeds: [
double(:production, attributes: { environment: 'production' })
])
]
end
let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2)}
let(:command) do
double(:command,
project: project,
stage_seeds: stage_seeds,
pipeline_seed: pipeline_seed,
save_incompleted: true
)
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let(:stages_attributes) do
[
{
name: 'build',
index: 0,
builds: [
{ name: 'init', scheduling_type: :stage },
{ name: 'build', scheduling_type: :stage }
]
},
{
name: 'test',
index: 1,
builds: [
{ name: 'rspec', scheduling_type: :stage },
{ name: 'staging', scheduling_type: :stage, environment: 'staging' },
{ name: 'deploy', scheduling_type: :stage, environment: 'production' }
]
}
]
end
subject(:seed) do
described_class.new(pipeline, stages_attributes)
end
describe '#stages' do
it 'returns the stage resources' do
stages = seed.stages
expect(stages).to all(be_a(Ci::Stage))
expect(stages.map(&:name)).to contain_exactly('build', 'test')
end
end
describe '#size' do
it 'returns the number of jobs' do
expect(seed.size).to eq(5)
end
end
describe '#errors' do
context 'when attributes are valid' do
it 'returns nil' do
expect(seed.errors).to be_nil
end
end
context 'when attributes are not valid' do
it 'returns the errors' do
stages_attributes[0][:builds] << {
name: 'invalid_job',
scheduling_type: :dag,
needs_attributes: [{ name: 'non-existent', artifacts: true }]
}
expect(seed.errors).to contain_exactly("invalid_job: needs 'non-existent'")
end
end
end
describe '#deployments_count' do
it 'counts the jobs having an environment associated' do
expect(seed.deployments_count).to eq(2)
end
end
end

View File

@ -1255,45 +1255,21 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
describe 'aggregated_metrics' do
shared_examples 'aggregated_metrics_for_time_range' do
context 'with product_analytics_aggregated_metrics feature flag on' do
before do
stub_feature_flags(product_analytics_aggregated_metrics: true)
end
describe '.aggregated_metrics_weekly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly }
it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(aggregated_metrics_data_method).and_return(global_search_gmau: 123)
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
context 'with product_analytics_aggregated_metrics feature flag off' do
before do
stub_feature_flags(product_analytics_aggregated_metrics: false)
end
it 'returns empty hash', :aggregate_failures do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(aggregated_metrics_data_method)
expect(aggregated_metrics_payload).to be {}
end
end
it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_weekly_data).and_return(global_search_gmau: 123)
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
describe '.aggregated_metrics_weekly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly }
describe '.aggregated_metrics_monthly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
let(:aggregated_metrics_data_method) { :aggregated_metrics_weekly_data }
it_behaves_like 'aggregated_metrics_for_time_range'
end
describe '.aggregated_metrics_monthly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
let(:aggregated_metrics_data_method) { :aggregated_metrics_monthly_data }
it_behaves_like 'aggregated_metrics_for_time_range'
it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_monthly_data).and_return(global_search_gmau: 123)
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/policy_rule_boolean'
RSpec.describe RuboCop::Cop::Gitlab::PolicyRuleBoolean, type: :rubocop do
include CopHelper
subject(:cop) { described_class.new }
it 'registers offense for &&' do
expect_offense(<<~SOURCE)
rule { conducts_electricity && batteries }.enable :light_bulb
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ && is not allowed within a rule block. Did you mean to use `&`?
SOURCE
end
it 'registers offense for ||' do
expect_offense(<<~SOURCE)
rule { conducts_electricity || batteries }.enable :light_bulb
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ || is not allowed within a rule block. Did you mean to use `|`?
SOURCE
end
it 'registers offense for if' do
expect_offense(<<~SOURCE)
rule { if conducts_electricity then can?(:magnetize) else batteries end }.enable :motor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ if and ternary operators are not allowed within a rule block.
SOURCE
end
it 'registers offense for ternary operator' do
expect_offense(<<~SOURCE)
rule { conducts_electricity ? can?(:magnetize) : batteries }.enable :motor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ if and ternary operators are not allowed within a rule block.
SOURCE
end
it 'registers no offense for &' do
expect_no_offenses(<<~SOURCE)
rule { conducts_electricity & batteries }.enable :light_bulb
SOURCE
end
it 'registers no offense for |' do
expect_no_offenses(<<~SOURCE)
rule { conducts_electricity | batteries }.enable :light_bulb
SOURCE
end
end

View File

@ -5,19 +5,20 @@ require 'spec_helper'
RSpec.describe StubFeatureFlags do
let_it_be(:dummy_feature_flag) { :dummy_feature_flag }
let_it_be(:dummy_definition) do
Feature::Definition.new(
nil,
name: dummy_feature_flag,
type: 'development',
default_enabled: false
)
end
# We inject dummy feature flag defintion
# to ensure that we strong validate it's usage
# as well
before(:all) do
definition = Feature::Definition.new(
nil,
name: dummy_feature_flag,
type: 'development',
# we allow ambigious usage of `default_enabled:`
default_enabled: [false, true]
)
Feature::Definition.definitions[dummy_feature_flag] = definition
Feature::Definition.definitions[dummy_feature_flag] = dummy_definition
end
after(:all) do
@ -47,6 +48,10 @@ RSpec.describe StubFeatureFlags do
it { expect(Feature.disabled?(feature_name)).not_to eq(expected_result) }
context 'default_enabled does not impact feature state' do
before do
allow(dummy_definition).to receive(:default_enabled).and_return(true)
end
it { expect(Feature.enabled?(feature_name, default_enabled: true)).to eq(expected_result) }
it { expect(Feature.disabled?(feature_name, default_enabled: true)).not_to eq(expected_result) }
end
@ -79,6 +84,10 @@ RSpec.describe StubFeatureFlags do
it { expect(Feature.disabled?(feature_name, actor(tested_actor))).not_to eq(expected_result) }
context 'default_enabled does not impact feature state' do
before do
allow(dummy_definition).to receive(:default_enabled).and_return(true)
end
it { expect(Feature.enabled?(feature_name, actor(tested_actor), default_enabled: true)).to eq(expected_result) }
it { expect(Feature.disabled?(feature_name, actor(tested_actor), default_enabled: true)).not_to eq(expected_result) }
end

View File

@ -8,6 +8,13 @@ RSpec.describe GitlabUsagePingWorker, :clean_gitlab_redis_shared_state do
allow(subject).to receive(:sleep)
end
it 'does not run for GitLab.com' do
allow(Gitlab).to receive(:com?).and_return(true)
expect(SubmitUsagePingService).not_to receive(:new)
subject.perform
end
it 'delegates to SubmitUsagePingService' do
expect_next_instance_of(SubmitUsagePingService) { |service| expect(service).to receive(:execute) }