Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-17 18:16:07 +00:00
parent 8432be20de
commit d409d12963
69 changed files with 368 additions and 2223 deletions

View File

@ -5,4 +5,3 @@ Gitlab/DelegatePredicateMethods:
- app/models/concerns/integrations/base_data_fields.rb
- app/models/project.rb
- ee/app/models/concerns/ee/ci/metadatable.rb
- lib/gitlab/ci/trace/stream.rb

View File

@ -1,4 +0,0 @@
---
GraphQL/ArgumentName:
Exclude:
- ee/app/graphql/mutations/audit_events/external_audit_event_destinations/update.rb

View File

@ -31,11 +31,6 @@ Rails/SaveBang:
- spec/lib/gitlab/database/custom_structure_spec.rb
- spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
- spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
- spec/lib/gitlab/gfm/reference_rewriter_spec.rb
- spec/lib/gitlab/git_access_spec.rb
- spec/lib/gitlab/import_export/avatar_saver_spec.rb
- spec/lib/gitlab/import_export/base/relation_factory_spec.rb
- spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
- spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
- spec/lib/gitlab/import_export/fork_spec.rb
- spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb

View File

@ -1673,4 +1673,4 @@ DEPENDENCIES
yajl-ruby (~> 1.4.1)
BUNDLED WITH
2.1.4
2.3.5

View File

@ -81,7 +81,7 @@ export default {
</blob-filepath>
</div>
<div class="gl-display-none gl-sm-display-flex">
<div class="gl-display-none gl-sm-display-flex file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>

View File

@ -1,3 +0,0 @@
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
initWorkItemsHierarchy();

View File

@ -3,6 +3,7 @@ import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import getRefMixin from '~/repository/mixins/get_ref';
import initSourcegraph from '~/sourcegraph';
import { updateElementsVisibility } from '../utils/dom';
import blobControlsQuery from '../queries/blob_controls.query.graphql';
@ -76,6 +77,9 @@ export default {
showBlobControls(shouldShow) {
updateElementsVisibility('.tree-controls', !shouldShow);
},
blobInfo() {
initSourcegraph();
},
},
};
</script>
@ -97,6 +101,7 @@ export default {
data-testid="permalink"
:href="blobInfo.permalinkPath"
:class="$options.buttonClassList"
class="js-data-file-blob-permalink-url"
>
{{ $options.i18n.permalink }}
</gl-button>

View File

@ -2,7 +2,6 @@
import { GlSafeHtmlDirective } from '@gitlab/ui';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import { sanitize } from '~/lib/dompurify';
import '~/sourcegraph/load';
const LINE_SELECT_CLASS_NAME = 'hll';

View File

@ -1,54 +1,5 @@
import { __ } from '~/locale';
export const widgetTypes = {
title: 'TITLE',
};
export const WI_TITLE_TRACK_LABEL = 'item_title';
export const workItemTypes = {
EPIC: {
title: __('Epic'),
icon: 'epic',
color: '#694CC0',
backgroundColor: '#E1D8F9',
},
ISSUE: {
title: __('Issue'),
icon: 'issues',
color: '#1068BF',
backgroundColor: '#CBE2F9',
},
TASK: {
title: __('Task'),
icon: 'task-done',
color: '#217645',
backgroundColor: '#C3E6CD',
},
INCIDENT: {
title: __('Incident'),
icon: 'issue-type-incident',
backgroundColor: '#db2a0f',
color: '#FDD4CD',
iconSize: 16,
},
SUB_EPIC: {
title: __('Child epic'),
icon: 'epic',
color: '#AB6100',
backgroundColor: '#F5D9A8',
},
REQUIREMENT: {
title: __('Requirement'),
icon: 'requirements',
color: '#0068c5',
backgroundColor: '#c5e3fb',
},
TEST_CASE: {
title: __('Test case'),
icon: 'issue-type-test-case',
backgroundColor: '#007a3f',
color: '#bae8cb',
iconSize: 16,
},
};

View File

@ -1,96 +0,0 @@
<script>
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { workItemTypes } from '~/work_items/constants';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME } from '../constants';
import Hierarchy from './hierarchy.vue';
export default {
components: {
GlBanner,
Hierarchy,
},
inject: ['illustrationPath', 'licensePlan'],
data() {
return {
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
workItemHierarchy: RESPONSE[this.licensePlan],
};
},
computed: {
hasUnavailableStructure() {
return this.workItemTypes.unavailable.length > 0;
},
workItemTypes() {
return this.workItemHierarchy.reduce(
(itemTypes, item) => {
const key = item.available ? 'available' : 'unavailable';
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
},
},
methods: {
handleClose() {
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
this.bannerVisible = false;
},
},
};
</script>
<template>
<div>
<gl-banner
v-if="bannerVisible"
class="gl-mt-4 gl-px-5!"
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
:button-text="s__('Hierarchy|Take the work items survey')"
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
:svg-path="illustrationPath"
@close="handleClose"
>
<p>
{{
s__(
'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
)
}}
</p>
</gl-banner>
<h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
<p>
{{
s__(
'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
)
}}
</p>
<div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
<p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
<hierarchy :work-item-types="workItemTypes.available" />
<div
v-if="hasUnavailableStructure"
data-testid="unavailable-structure"
class="gl-font-weight-bold gl-mt-5 gl-mb-2"
>
{{ s__('Hierarchy|Unavailable structure') }}
</div>
<p v-if="hasUnavailableStructure" class="gl-mb-3!">
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
</p>
<hierarchy :work-item-types="workItemTypes.unavailable" />
</div>
</template>

View File

@ -1,119 +0,0 @@
<script>
import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlBadge,
},
props: {
workItemTypes: {
type: Array,
required: true,
},
},
methods: {
isLastItem(index, workItem) {
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
return isLastItemInArray && hasMoreThanOneItem;
},
nestedWorkItemTypeMargin(index, workItem) {
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
if (isLastItemInArray && hasMoreThanOneItem) {
return 'gl-ml-0';
}
return 'gl-ml-6';
},
},
};
</script>
<template>
<div>
<div
v-for="workItem in workItemTypes"
:key="workItem.id"
class="gl-mb-3"
:class="{ flex: !workItem.available }"
>
<span
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
data-testid="work-item-wrapper"
>
<span
:style="{
backgroundColor: workItem.backgroundColor,
color: workItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
</span>
{{ workItem.title }}
</span>
<gl-badge
v-if="!workItem.available"
variant="info"
icon="license"
size="sm"
class="gl-ml-3 gl-align-self-center"
>{{ workItem.license }}</gl-badge
>
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
<svg
v-if="workItem.nestedTypes.length > 1"
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
data-testid="hierarchy-rounded-arrow-tail"
width="2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="0.75"
y1="1"
x2="0.75"
y2="100%"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
<div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
<gl-icon name="arrow-down" class="gl-text-gray-400" />
</div>
<gl-icon
v-if="isLastItem(index, workItem)"
:key="nestedWorkItem.id"
name="level-up"
class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
/>
<span
:key="nestedWorkItem.id"
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
:class="nestedWorkItemTypeMargin(index, workItem)"
>
<span
:style="{
backgroundColor: nestedWorkItem.backgroundColor,
color: nestedWorkItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
</span>
{{ nestedWorkItem.title }}
</span>
</template>
</div>
</div>
</div>
</template>

View File

@ -1,12 +0,0 @@
export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
/**
* Hard-coded strings since we're rendering hierarchy
* items from mock responses. Remove this when we
* have a real hierarchy endpoint.
*/
export const LICENSE_PLAN = {
FREE: 'free',
PREMIUM: 'premium',
ULTIMATE: 'ultimate',
};

View File

@ -1,10 +0,0 @@
import { LICENSE_PLAN } from './constants';
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
if (hasSubEpics) {
return LICENSE_PLAN.ULTIMATE;
} else if (hasEpics) {
return LICENSE_PLAN.PREMIUM;
}
return LICENSE_PLAN.FREE;
}

View File

@ -1,142 +0,0 @@
const FREE_TIER = 'free';
const ULTIMATE_TIER = 'ultimate';
const PREMIUM_TIER = 'premium';
const RESPONSE = {
[FREE_TIER]: [
{
id: '1',
type: 'ISSUE',
available: true,
license: null,
nestedTypes: null,
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '4',
type: 'EPIC',
available: false,
license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[PREMIUM_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[ULTIMATE_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['SUB_EPIC', 'ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: true,
license: null,
nestedTypes: null,
},
],
};
export default RESPONSE;

View File

@ -1,26 +0,0 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import { inferLicensePlan } from './hierarchy_util';
export const initWorkItemsHierarchy = () => {
const el = document.querySelector('#js-work-items-hierarchy');
const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
const licensePlan = inferLicensePlan({
hasEpics: parseBoolean(hasEpics),
hasSubEpics: parseBoolean(hasSubEpics),
});
return new Vue({
el,
provide: {
illustrationPath,
licensePlan,
},
render(createElement) {
return createElement(App);
},
});
};

View File

@ -32,4 +32,3 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
@import './pages/hierarchy';

View File

@ -255,18 +255,23 @@ span.highlight_word {
.hll { background-color: $white-hll-bg; }
.c { color: $white-c;
.c,
.hljs-comment { color: $white-c;
font-style: italic; }
.err { color: $white-err;
background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.k,
.hljs-variable.language_,
.hljs-built_in { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm;
font-style: italic; }
.cp { color: $white-cp;
.cp,
.hljs-meta { color: $white-cp;
font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1;
@ -310,20 +315,34 @@ span.highlight_word {
font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kd,
.hljs-keyword { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
.kt { color: $white-kt;
.kt,
.hljs-type { color: $white-kt;
font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
.nc { color: $white-nc;
.n,
.hljs-built_in { color: $white-n; }
.na,
.hljs-attr,
.hljs-property,
.hljs-title.function_ { color: $white-na; }
.nb,
.hljs-title.class_,
.hljs-literal { color: $white-nb; }
.nc,
.hljs-title.class_,
.hljs-built_in { color: $white-nc;
font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
@ -331,7 +350,9 @@ span.highlight_word {
.ne { color: $white-ne;
font-weight: $gl-font-weight-bold; }
.nf { color: $white-nf;
.nf,
.hljs-title,
.hljs-title.function_ { color: $white-nf;
font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
@ -340,7 +361,9 @@ span.highlight_word {
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
.mi { color: $white-mi; }
.mi,
.hljs-number { color: $white-mi; }
.mo { color: $white-mo; }
.sb { color: $white-sb; }
.sc { color: $white-sc; }
@ -351,7 +374,9 @@ span.highlight_word {
.si { color: $white-si; }
.sx { color: $white-sx; }
.sr { color: $white-sr; }
.s1 { color: $white-s1; }
.s1,
.hljs-string { color: $white-s1; }
.ss { color: $white-ss; }
.bp { color: $white-bp; }
.vc { color: $white-vc; }

View File

@ -1,15 +0,0 @@
.hierarchy-rounded-arrow-tail {
position: absolute;
top: 4px;
left: 5px;
height: calc(100% - 20px);
}
.hierarchy-icon-wrapper {
height: $default-icon-size;
width: $default-icon-size;
}
.hierarchy-rounded-arrow {
transform: scale(1, -1) rotate(90deg);
}

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
module WorkItemsHierarchy
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def planning_hierarchy
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
render 'shared/planning_hierarchy'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')

View File

@ -6,6 +6,7 @@ class Projects::TreeController < Projects::ApplicationController
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
include RedirectsForMissingPathOnTree
include SourcegraphDecorator
around_action :allow_gitaly_ref_name_caching, only: [:show]

View File

@ -9,7 +9,7 @@ class ProjectsController < Projects::ApplicationController
include RecordUserLastActivity
include ImportUrlParams
include FiltersEvents
include WorkItemsHierarchy
include SourcegraphDecorator
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@ -53,7 +53,6 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names]
feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:refs]
urgency :high, [:unfoldered_environment_names]

View File

@ -69,6 +69,7 @@ module Ci
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :generic_commit_statuses, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus'
has_many :job_artifacts, through: :builds
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
has_many :deployments, through: :builds
@ -130,6 +131,7 @@ module Ci
after_create :keep_around_commits, unless: :importing?
use_fast_destroy :job_artifacts
use_fast_destroy :build_trace_chunks
# We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend
# this `Hash` with new values.

View File

@ -240,7 +240,6 @@ class ProjectPolicy < BasePolicy
enable :read_wiki
enable :read_issue
enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
@ -573,7 +572,6 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list
enable :read_wiki
enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member

View File

@ -3,39 +3,50 @@
module Ci
class AfterRequeueJobService < ::BaseService
def execute(processable)
process_subsequent_jobs(processable)
reset_source_bridge(processable)
@processable = processable
process_subsequent_jobs
reset_source_bridge
end
private
def process_subsequent_jobs(processable)
(stage_dependent_jobs(processable) | needs_dependent_jobs(processable))
.each do |processable|
process(processable)
def process_subsequent_jobs
dependent_jobs.each do |job|
process(job)
end
end
def reset_source_bridge(processable)
processable.pipeline.reset_source_bridge!(current_user)
def reset_source_bridge
@processable.pipeline.reset_source_bridge!(current_user)
end
def process(processable)
Gitlab::OptimisticLocking.retry_lock(processable, name: 'ci_requeue_job') do |processable|
processable.process(current_user)
def dependent_jobs
if ::Feature.enabled?(:ci_order_subsequent_jobs_by_stage, @processable.pipeline.project, default_enabled: :yaml)
stage_dependent_jobs
.or(needs_dependent_jobs.except(:preload))
.ordered_by_stage
else
stage_dependent_jobs | needs_dependent_jobs
end
end
def skipped_jobs(processable)
processable.pipeline.processables.skipped
def process(job)
Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_requeue_job') do |job|
job.process(current_user)
end
end
def stage_dependent_jobs(processable)
skipped_jobs(processable).after_stage(processable.stage_idx)
def stage_dependent_jobs
skipped_jobs.after_stage(@processable.stage_idx)
end
def needs_dependent_jobs(processable)
skipped_jobs(processable).scheduling_type_dag.with_needs([processable.name])
def needs_dependent_jobs
skipped_jobs.scheduling_type_dag.with_needs([@processable.name])
end
def skipped_jobs
@skipped_jobs ||= @processable.pipeline.processables.skipped
end
end
end

View File

@ -9,9 +9,11 @@ module Ci
pipeline.cancel_running if pipeline.cancelable?
# Ci::Pipeline#destroy triggers `use_fast_destroy :job_artifacts` and
# ci_builds has ON DELETE CASCADE to ci_pipelines. The pipeline, the builds,
# job and pipeline artifacts all get destroyed here.
# The pipeline, the builds, job and pipeline artifacts all get destroyed here.
# Ci::Pipeline#destroy triggers fast destroy on job_artifacts and
# build_trace_chunks to remove the records and data stored in object storage.
# ci_builds records are deleted using ON DELETE CASCADE from ci_pipelines
#
pipeline.reset.destroy!
ServiceResponse.success(message: 'Pipeline not found')

View File

@ -1,5 +0,0 @@
- page_title _("Planning hierarchy")
- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
- has_epics = Gitlab.ee? && @project&.feature_available?(:epics)
#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }

View File

@ -1,8 +1,8 @@
---
name: work_items_hierarchy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76720
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
name: ci_order_subsequent_jobs_by_stage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77528
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349977
milestone: '14.7'
type: development
group: group::product planning
group: group::pipeline authoring
default_enabled: false

View File

@ -641,7 +641,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :generate_new_export
get :download_export
get :activity
get :planning_hierarchy
get :refs
put :new_issuable_address
get :unfoldered_environment_names

View File

@ -19,6 +19,7 @@ The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/g
| `project_id` | **{dotted-circle}** | integer | |
| `namespace_id` | **{dotted-circle}** | integer | |
| `user_id` | **{dotted-circle}** | integer | User database record ID attribute. This file undergoes a pseudonymization process at the collector level. |
| `context_generated_at` | **{dotted-circle}** | string (date time format) | Timestamp indicating when context was generated. |
| `environment` | **{check-circle}** | string (max 32 chars) | Name of the source environment, such as `production` or `staging` |
| `source` | **{check-circle}** | string (max 32 chars) | Name of the source application, such as `gitlab-rails` or `gitlab-javascript` |
| `plan` | **{dotted-circle}** | string (max 32 chars) | Name of the plan for the namespace, such as `free`, `premium`, or `ultimate`. Automatically picked from the `namespace`. |

View File

@ -55,14 +55,21 @@ you can increase the values to complete backfill in a shorter time. If it's
under heavy load and backfill reduces its availability for normal requests,
you can decrease them.
## Using a different URL for synchronization
## Set up the internal URLs
> Setting up internal URLs in secondary sites was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77179) in GitLab 14.7.
You can set up a different URL for synchronization between the primary and secondary site.
The **primary** site's Internal URL is used by **secondary** sites to contact it
(to sync repositories, for example). The name Internal URL distinguishes it from
[External URL](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-the-external-url-for-gitlab),
which is used by users. Internal URL does not need to be a private address.
Internal URL defaults to external URL, but you can also customize it:
When [Geo secondary proxying](../../administration/geo/secondary_proxy/index.md) is enabled,
the primary uses the secondary's internal URL to contact it directly.
The internal URL defaults to external URL. To change it:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Geo > Nodes**.
@ -70,6 +77,9 @@ Internal URL defaults to external URL, but you can also customize it:
1. Edit the internal URL.
1. Select **Save changes**.
When enabled, the Admin Area for Geo shows replication details for each site directly
from the primary site's UI, and through the Geo secondary proxy, if enabled.
WARNING:
We recommend using an HTTPS connection while configuring the Geo sites. To avoid
breaking communication between **primary** and **secondary** sites when using

View File

@ -313,10 +313,10 @@ Use this schema to define `clusters` objects in the [`schedule` rule type](#sche
| Field | Type | Possible values | Description |
|--------------|---------------------|--------------------------|-------------|
| `containers` | `array` of `string` | | The container name to be scanned (only the first value is currently supported). |
| `resources` | `array` of `string` | | The resource name to be scanned (only the first value is currently supported). |
| `namespaces` | `array` of `string` | | The namespace to be scanned (only the first value is currently supported). |
| `kinds` | `array` of `string` | `deployment`/`daemonset` | The resource kind to be scanned (only the first value is currently supported). |
| `containers` | `array` of `string` | | The container name that is scanned (only the first value is currently supported). |
| `resources` | `array` of `string` | | The resource name that is scanned (only the first value is currently supported). |
| `namespaces` | `array` of `string` | | The namespace that is scanned (only the first value is currently supported). |
| `kinds` | `array` of `string` | `deployment`/`daemonset` | The resource kind that should be scanned (only the first value is currently supported). |
### `scan` action type

View File

@ -100,6 +100,14 @@ You can give a user access to all projects in a group.
1. Fill in the fields.
- The role applies to all projects in the group. [Learn more about permissions](../permissions.md).
- On the **Access expiration date**, the user can no longer access projects in the group.
1. Select **Invite**.
Members that are not automatically added are displayed on the **Invited** tab.
Users can be on this tab because they:
- Have not yet accepted the invitation.
- Are waiting for [approval from an administrator](../admin_area/moderate_users.md).
- [Exceed the group user cap](#user-cap-for-groups).
## Request access to a group
@ -508,6 +516,74 @@ To prevent a project from being shared with other groups:
This setting applies to all subgroups unless overridden by a group owner. Groups already
added to a project lose access when the setting is enabled.
## User cap for groups
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330027) in GitLab 14.7.
FLAG:
On self-managed GitLab, this feature is not available. On GitLab.com, this feature is available for some groups.
This feature is not ready for production use.
When the number of billable members reaches the user cap, new users can't be added to the group
without being approved by the group owner.
Groups with the user cap feature enabled have [group sharing](#share-a-group-with-another-group)
disabled for the group and its subgroups.
### Specify a user cap for a group
Prerequisite:
- You must be assigned the [Owner role](../permissions.md#group-members-permissions) for the group.
To specify a user cap:
1. On the top bar, select **Menu > Groups** and find your group.
You can set a cap on the top-level group only.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions and group features**.
1. In the **User cap** box, enter the desired number of users.
1. Select **Save changes**.
If you already have more users in the group than the user cap value, users
are not removed. However, you can't add more without approval.
Increasing the user cap does not approve pending members.
### Remove the user cap for a group
You can remove the user cap, so there is no limit on the number of members you can add to a group.
Prerequisite:
- You must be assigned the [Owner role](../permissions.md#group-members-permissions) for the group.
To remove the user cap:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions and group features**.
1. In the **User cap** box, delete the value.
1. Select **Save changes**.
Decreasing the user cap does not approve pending members.
### Approve pending members for a group
When the number of billable users reaches the user cap, any new member is put in a pending state
and must be approved.
Prerequisite:
- You must be assigned the [Owner role](../permissions.md#group-members-permissions) for the group.
To approve members that are pending because they've exceeded the user cap:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Usage Quotas**.
1. On the **Seats** tab, under the alert, select **View pending approvals**.
1. For each member you want to approve, select **Approve**.
## Prevent members from being added to projects in a group **(PREMIUM)**
As a group owner, you can prevent any new project membership for all

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -20,20 +20,6 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
portfolio management, see
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
## View planning hierarchies
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.7 and is behind the feature flag `work_items_hierarchy`.
To view the planning hierarchy in a project:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Project information > Planning hierarchy**.
Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy.
The work items outside your subscription plan show up below **Unavailable structure**.
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_7.png)
## Hierarchies with epics
With epics, you can achieve the following hierarchy:

View File

@ -11,10 +11,6 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
delegate :valid?, to: :stream, allow_nil: true
alias_method :present?, :valid?
def initialize(metrics = Trace::Metrics.new)
@stream = yield
@stream&.binmode
@ -24,6 +20,7 @@ module Gitlab
def valid?
self.stream.present?
end
alias_method :present?, :valid?
def file?
self.path.present?

View File

@ -3,7 +3,7 @@
module Gitlab
module Tracking
class StandardContext
GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-7'
GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-8'
GITLAB_RAILS_SOURCE = 'gitlab-rails'
def initialize(namespace: nil, project: nil, user: nil, **extra)
@ -46,7 +46,8 @@ module Gitlab
extra: extra,
user_id: user&.id,
namespace_id: namespace&.id,
project_id: project_id
project_id: project_id,
context_generated_at: Time.current
}
end

View File

@ -1,27 +0,0 @@
# frozen_string_literal: true
# This module has the necessary methods to render
# work items hierarchy menu
module Sidebars
module Concerns
module WorkItemHierarchy
def hierarchy_menu_item(container, url, path)
unless show_hierarachy_menu_item?(container)
return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
end
::Sidebars::MenuItem.new(
title: _('Planning hierarchy'),
link: url,
active_routes: { path: path },
item_id: :hierarchy
)
end
def show_hierarachy_menu_item?(container)
Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
can?(context.current_user, :read_work_items_hierarchy, container)
end
end
end
end

View File

@ -4,13 +4,10 @@ module Sidebars
module Projects
module Menus
class ProjectInformationMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::WorkItemHierarchy
override :configure_menu_items
def configure_menu_items
add_item(activity_menu_item)
add_item(labels_menu_item)
add_item(hierarchy_menu_item(context.project, planning_hierarchy_project_path(context.project), 'projects#planning_hierarchy'))
add_item(members_menu_item)
true

View File

@ -722,6 +722,9 @@ msgstr ""
msgid "%{level_name} is not allowed since the fork source project has lower visibility."
msgstr ""
msgid "%{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "%{link_start}Learn more%{link_end} about roles."
msgstr ""
@ -2480,6 +2483,12 @@ msgstr ""
msgid "AdminDashboard|Error loading the statistics. Please try again"
msgstr ""
msgid "AdminGeo|The URL of the primary site that is used internally by the secondary sites."
msgstr ""
msgid "AdminGeo|The URL of the secondary site that is used internally by the primary site."
msgstr ""
msgid "AdminLabels|Define your default set of project labels"
msgstr ""
@ -7071,9 +7080,6 @@ msgstr ""
msgid "Child"
msgstr ""
msgid "Child epic"
msgstr ""
msgid "Child epic does not exist."
msgstr ""
@ -17619,33 +17625,6 @@ msgstr[1] ""
msgid "Hide values"
msgstr ""
msgid "Hierarchy|Current structure"
msgstr ""
msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
msgstr ""
msgid "Hierarchy|Help us improve work items in GitLab!"
msgstr ""
msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
msgstr ""
msgid "Hierarchy|Planning hierarchy"
msgstr ""
msgid "Hierarchy|Take the work items survey"
msgstr ""
msgid "Hierarchy|These items are unavailable in the current structure."
msgstr ""
msgid "Hierarchy|Unavailable structure"
msgstr ""
msgid "Hierarchy|You can start using these items now."
msgstr ""
msgid "High or unknown vulnerabilities present"
msgstr ""
@ -26530,9 +26509,6 @@ msgstr ""
msgid "Plan:"
msgstr ""
msgid "Planning hierarchy"
msgstr ""
msgid "PlantUML"
msgstr ""
@ -30256,9 +30232,6 @@ msgstr ""
msgid "Required only if you are not using role instance credentials."
msgstr ""
msgid "Requirement"
msgstr ""
msgid "Requirement %{reference} has been added"
msgstr ""
@ -34942,9 +34915,6 @@ msgstr ""
msgid "Target-Branch"
msgstr ""
msgid "Task"
msgstr ""
msgid "Task ID: %{elastic_task}"
msgstr ""
@ -35190,9 +35160,6 @@ msgstr ""
msgid "Test Cases"
msgstr ""
msgid "Test case"
msgstr ""
msgid "Test coverage parsing"
msgstr ""
@ -35406,12 +35373,6 @@ msgstr ""
msgid "The Snowplow cookie domain."
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it."
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "The URL of the Jenkins server."
msgstr ""

View File

@ -63,6 +63,7 @@ WORKDIR /home/gitlab/qa
# Install qa dependencies or fetch from cache if unchanged
COPY ./qa/Gemfile* /home/gitlab/qa/
RUN gem install bundler --no-document --conservative --version 2.3.5
RUN bundle install --jobs=$(nproc) --retry=3 --without=development --quiet
##

View File

@ -355,4 +355,4 @@ DEPENDENCIES
zeitwerk (~> 2.4)
BUNDLED WITH
2.2.33
2.3.5

View File

@ -36,6 +36,7 @@ function bundle_install_script() {
exit 1;
fi;
gem install bundler --no-document --conservative --version 2.3.5
bundle --version
bundle config set path "$(pwd)/vendor"
bundle config set clean 'true'

View File

@ -47,7 +47,7 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
if @pid
pgrp = Process.getpgid(@pid)
Timeout.timeout(5) do
Timeout.timeout(10) do
Process.kill('TERM', -pgrp)
Process.waitpid(@pid)
end
@ -63,7 +63,7 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
it 'serves /metrics endpoint' do
expect do
Timeout.timeout(5) do
Timeout.timeout(10) do
http_ok = false
until http_ok
sleep 1

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItemsHierarchy do
controller(ApplicationController) do
include WorkItemsHierarchy
end
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
render_views
before do
sign_in user
routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" }
controller.instance_variable_set(:@project, project)
end
it 'renders hierarchy' do
stub_feature_flags(work_items_hierarchy: true)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to match(/id="js-work-items-hierarchy"/)
end
it 'renders 404' do
stub_feature_flags(work_items_hierarchy: false)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
end
end

View File

@ -5,6 +5,7 @@ FactoryBot.define do
name { 'test' }
add_attribute(:protected) { false }
created_at { 'Di 29. Okt 09:50:00 CET 2013' }
scheduling_type { 'stage' }
pending
options do
@ -33,6 +34,8 @@ FactoryBot.define do
end
trait :dependent do
scheduling_type { 'dag' }
transient do
sequence(:needed_name) { |n| "dependency #{n}" }
needed { association(:ci_build, name: needed_name, pipeline: pipeline) }

View File

@ -1,7 +0,0 @@
/* useful for timing promises when jest fakeTimers are not reliable enough */
export default (timeout) =>
new Promise((resolve) => {
jest.useRealTimers();
setTimeout(resolve, timeout);
jest.useFakeTimers();
});

View File

@ -17,7 +17,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
</div>
<div
class="gl-display-none gl-sm-display-flex"
class="gl-display-none gl-sm-display-flex file-actions"
>
<viewer-switcher-stub
value="simple"

View File

@ -5,7 +5,6 @@ import Vue from 'vue';
import Vuex from 'vuex';
import '~/behaviors/markdown/render_gfm';
import waitForPromises from 'helpers/wait_for_promises';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
@ -540,7 +539,6 @@ describe('RepoEditor', () => {
},
});
await vm.$nextTick();
await vm.$nextTick();
expect(vm.initEditor).toHaveBeenCalled();
});
@ -567,8 +565,8 @@ describe('RepoEditor', () => {
// switching from edit to diff mode usually triggers editor initialization
vm.$store.state.viewer = viewerTypes.diff;
// we delay returning the file to make sure editor doesn't initialize before we fetch file content
await waitUsingRealTimer(30);
jest.runOnlyPendingTimers();
return 'rawFileData123\n';
});
@ -598,8 +596,9 @@ describe('RepoEditor', () => {
return aContent;
})
.mockImplementationOnce(async () => {
// we delay returning fileB content to make sure the editor doesn't initialize prematurely
await waitUsingRealTimer(30);
// we delay returning fileB content
// to make sure the editor doesn't initialize prematurely
jest.advanceTimersByTime(30);
return bContent;
});

View File

@ -1,78 +0,0 @@
import { nextTick } from 'vue';
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBanner } from '@gitlab/ui';
import App from '~/work_items_hierarchy/components/app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('WorkItemsHierarchy App', () => {
let wrapper;
const createComponent = (props = {}, data = {}) => {
wrapper = extendedWrapper(
mount(App, {
localVue,
provide: {
illustrationPath: '/foo.svg',
licensePlan: 'free',
...props,
},
data() {
return data;
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
licensePlan
${'free'}
${'premium'}
${'ultimate'}
`('when licensePlan is $licensePlan', ({ licensePlan }) => {
beforeEach(() => {
createComponent({ licensePlan });
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('survey banner', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
expect(wrapper.find(GlBanner).exists()).toBe(true);
});
it('hide when close is called', async () => {
createComponent({}, { bannerVisible: true });
wrapper.findByTestId('close-icon').trigger('click');
await nextTick();
expect(wrapper.find(GlBanner).exists()).toBe(false);
});
});
describe('Unavailable structure', () => {
it.each`
licensePlan | visible
${'free'} | ${true}
${'premium'} | ${true}
${'ultimate'} | ${false}
`('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
createComponent({ licensePlan });
expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
});
});
});

View File

@ -1,118 +0,0 @@
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBadge } from '@gitlab/ui';
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RESPONSE from '~/work_items_hierarchy/static_response';
import { workItemTypes } from '~/work_items/constants';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('WorkItemsHierarchy Hierarchy', () => {
let wrapper;
const workItemsFromResponse = (response) => {
return response.reduce(
(itemTypes, item) => {
const key = item.available ? 'available' : 'unavailable';
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
};
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(Hierarchy, {
localVue,
propsData: {
workItemTypes: props.workItemTypes,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('available structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.ultimate).available;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('does not render badges', () => {
expect(wrapper.find(GlBadge).exists()).toBe(false);
});
});
describe('unavailable structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.premium).unavailable;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('renders license badges for all work items', () => {
expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
});
it('does not render svg icon for linking', () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
});
});
describe('nested work items', () => {
describe.each`
licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
${'ultimate'} | ${true} | ${true} | ${true}
${'premium'} | ${false} | ${false} | ${true}
${'free'} | ${false} | ${false} | ${false}
`(
'when $licensePlan license',
({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE[licensePlan]).available;
createComponent({ workItemTypes: items });
});
it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
arrowTailVisible,
);
});
it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
});
it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
});
},
);
});
});

View File

@ -1,16 +0,0 @@
import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
describe('inferLicensePlan', () => {
it.each`
epics | subEpics | licensePlan
${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
${false} | ${false} | ${LICENSE_PLAN.FREE}
`(
'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
({ epics, subEpics, licensePlan }) => {
expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
},
);
});

View File

@ -92,7 +92,7 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
before do
old_project.update(namespace: old_group)
old_project.update!(namespace: old_group)
end
context 'label referenced by id' do

View File

@ -96,7 +96,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when the DeployKey has access to the project' do
before do
deploy_key.deploy_keys_projects.create(project: project, can_push: true)
deploy_key.deploy_keys_projects.create!(project: project, can_push: true)
end
it 'allows push and pull access' do
@ -820,7 +820,7 @@ RSpec.describe Gitlab::GitAccess do
project.add_role(user, role)
end
protected_branch.save
protected_branch.save!
aggregate_failures do
matrix.each do |action, allowed|
@ -1090,7 +1090,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when deploy_key can push' do
context 'when project is authorized' do
before do
key.deploy_keys_projects.create(project: project, can_push: true)
key.deploy_keys_projects.create!(project: project, can_push: true)
end
it { expect { push_access_check }.not_to raise_error }
@ -1120,7 +1120,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when deploy_key cannot push' do
context 'when project is authorized' do
before do
key.deploy_keys_projects.create(project: project, can_push: false)
key.deploy_keys_projects.create!(project: project, can_push: false)
end
it { expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:deploy_key_upload]) }

View File

@ -281,6 +281,7 @@ ci_pipelines:
- dast_site_profiles_pipeline
- package_build_infos
- package_file_build_infos
- build_trace_chunks
ci_refs:
- project
- ci_pipelines

View File

@ -20,7 +20,7 @@ RSpec.describe Gitlab::ImportExport::AvatarSaver do
end
it 'saves a project avatar' do
described_class.new(project: project_with_avatar, shared: shared).save
described_class.new(project: project_with_avatar, shared: shared).save # rubocop:disable Rails/SaveBang
expect(File).to exist(Dir["#{shared.export_path}/avatar/**/dk.png"].first)
end

View File

@ -11,7 +11,7 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
let(:excluded_keys) { [] }
subject do
described_class.create(relation_sym: relation_sym,
described_class.create(relation_sym: relation_sym, # rubocop:disable Rails/SaveBang
relation_hash: relation_hash,
relation_index: 1,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,

View File

@ -24,7 +24,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
allow(instance).to receive(:storage_path).and_return(export_path)
end
bundler.save
bundler.save # rubocop:disable Rails/SaveBang
end
after do

View File

@ -58,6 +58,10 @@ RSpec.describe Gitlab::Tracking::StandardContext do
expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
end
it 'contains context_generated_at timestamp', :freeze_time do
expect(snowplow_context.to_json.dig(:data, :context_generated_at)).to eq(Time.current)
end
context 'plan' do
context 'when namespace is not available' do
it 'is nil' do

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
shared_examples 'hierarchy menu' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled does not render' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled does render' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
describe 'Project hierarchy menu item' do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
it_behaves_like 'hierarchy menu'
end
end

View File

@ -59,25 +59,5 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil }
end
end
describe 'Hierarchy' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
end
end

View File

@ -49,9 +49,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
end
context 'FastDestroyAll' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project) }
let(:pipeline) { create(:ci_pipeline) }
let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
let(:subjects) { build.trace_chunks }
describe 'Forbid #destroy and #destroy_all' do
@ -84,13 +83,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
expect(external_data_counter).to be > 0
expect(subjects.count).to be > 0
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350185') do
# This should use to prevent cross-DB modification
# but due to https://gitlab.com/gitlab-org/gitlab/-/issues/350185
# the build trace chunks are not destroyed by Projects::DestroyService
# Change to: expect { Projects::DestroyService.new(project, project.owner).execute }.to eq(true)
expect { project.destroy! }.not_to raise_error
end
expect { pipeline.destroy! }.not_to raise_error
expect(subjects.count).to eq(0)
expect(external_data_counter).to eq(0)
@ -858,13 +851,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
context 'when project is destroyed' do
let(:subject) do
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350185') do
# This should use to prevent cross-DB modification
# but due to https://gitlab.com/gitlab-org/gitlab/-/issues/350185
# the build trace chunks are not destroyed by Projects::DestroyService
# Change to: Projects::DestroyService.new(project, project.owner).execute
project.destroy!
end
Projects::DestroyService.new(project, project.owner).execute
end
it_behaves_like 'deletes all build_trace_chunk and data in redis'

View File

@ -31,6 +31,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_many(:statuses_order_id_desc) }
it { is_expected.to have_many(:bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) }
it { is_expected.to have_many(:build_trace_chunks).through(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
it { is_expected.to have_many(:sourced_pipelines) }

View File

@ -8,46 +8,56 @@ RSpec.describe Ci::AfterRequeueJobService do
let(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 0, name: 'build') }
let!(:test1) { create(:ci_build, :success, pipeline: pipeline, stage_idx: 1) }
let!(:test2) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: 1) }
let!(:test3) { create(:ci_build, :skipped, :dependent, pipeline: pipeline, stage_idx: 1, needed: build) }
let!(:deploy) { create(:ci_build, :skipped, :dependent, pipeline: pipeline, stage_idx: 2, needed: test3) }
let!(:build1) { create(:ci_build, name: 'build1', pipeline: pipeline, stage_idx: 0) }
let!(:test1) { create(:ci_build, :success, name: 'test1', pipeline: pipeline, stage_idx: 1) }
let!(:test2) { create(:ci_build, :skipped, name: 'test2', pipeline: pipeline, stage_idx: 1) }
let!(:test3) { create(:ci_build, :skipped, :dependent, name: 'test3', pipeline: pipeline, stage_idx: 1, needed: build1) }
let!(:deploy) { create(:ci_build, :skipped, :dependent, name: 'deploy', pipeline: pipeline, stage_idx: 2, needed: test3) }
subject(:execute_service) { described_class.new(project, user).execute(build) }
subject(:execute_service) { described_class.new(project, user).execute(build1) }
it 'marks subsequent skipped jobs as processable' do
expect(test1.reload).to be_success
expect(test2.reload).to be_skipped
expect(test3.reload).to be_skipped
expect(deploy.reload).to be_skipped
shared_examples 'processing subsequent skipped jobs' do
it 'marks subsequent skipped jobs as processable' do
expect(test1.reload).to be_success
expect(test2.reload).to be_skipped
expect(test3.reload).to be_skipped
expect(deploy.reload).to be_skipped
execute_service
execute_service
expect(test1.reload).to be_success
expect(test2.reload).to be_created
expect(test3.reload).to be_created
expect(deploy.reload).to be_created
expect(test1.reload).to be_success
expect(test2.reload).to be_created
expect(test3.reload).to be_created
expect(deploy.reload).to be_created
end
end
it_behaves_like 'processing subsequent skipped jobs'
context 'when there is a job need from the same stage' do
let!(:test4) do
let!(:build2) do
create(:ci_build,
:skipped,
:dependent,
name: 'build2',
pipeline: pipeline,
stage_idx: 0,
scheduling_type: :dag,
needed: build)
needed: build1)
end
it 'marks subsequent skipped jobs as processable' do
expect { execute_service }.to change { test4.reload.status }.from('skipped').to('created')
shared_examples 'processing the same stage job' do
it 'marks subsequent skipped jobs as processable' do
expect { execute_service }.to change { build2.reload.status }.from('skipped').to('created')
end
end
it_behaves_like 'processing subsequent skipped jobs'
it_behaves_like 'processing the same stage job'
end
context 'when the pipeline is a downstream pipeline and the bridge is depended' do
let!(:trigger_job) { create(:ci_bridge, :strategy_depend, status: 'success') }
let!(:trigger_job) { create(:ci_bridge, :strategy_depend, name: 'trigger_job', status: 'success') }
before do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: trigger_job)

View File

@ -66,6 +66,28 @@ RSpec.describe ::Ci::DestroyPipelineService do
expect { subject }.to change { Ci::DeletedObject.count }
end
end
context 'when job has trace chunks' do
let(:connection_params) { Gitlab.config.artifacts.object_store.connection.symbolize_keys }
let(:connection) { ::Fog::Storage.new(connection_params) }
before do
stub_object_storage(connection_params: connection_params, remote_directory: 'artifacts')
stub_artifacts_object_storage
end
let!(:trace_chunk) { create(:ci_build_trace_chunk, :fog_with_data, build: build) }
it 'destroys associated trace chunks' do
subject
expect { trace_chunk.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'removes data from object store' do
expect { subject }.to change { Ci::BuildTraceChunks::Fog.new.data(trace_chunk) }
end
end
end
context 'when pipeline is in cancelable state' do

View File

@ -1004,6 +1004,63 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
end
end
context 'when the dependency is stage-independent', :sidekiq_inline do
let(:config) do
<<-EOY
stages: [A, B]
A1:
stage: A
script: exit 0
when: manual
A2:
stage: A
script: exit 0
needs: [A1]
B:
stage: B
needs: [A2]
script: exit 0
EOY
end
let(:pipeline) do
Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
end
before do
stub_ci_pipeline_yaml_file(config)
end
it 'processes subsequent jobs in the correct order when playing first job' do
expect(all_builds_names).to eq(%w[A1 A2 B])
expect(all_builds_statuses).to eq(%w[manual skipped skipped])
play_manual_action('A1')
expect(all_builds_names).to eq(%w[A1 A2 B])
expect(all_builds_statuses).to eq(%w[pending created created])
end
context 'when the FF ci_order_subsequent_jobs_by_stage is disabled' do
before do
stub_feature_flags(ci_order_subsequent_jobs_by_stage: false)
end
it 'processes subsequent jobs in an incorrect order when playing first job' do
expect(all_builds_names).to eq(%w[A1 A2 B])
expect(all_builds_statuses).to eq(%w[manual skipped skipped])
play_manual_action('A1')
expect(all_builds_names).to eq(%w[A1 A2 B])
expect(all_builds_statuses).to eq(%w[pending created skipped])
end
end
end
private
def all_builds

View File

@ -212,9 +212,7 @@ module TestEnv
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
Bundler.with_original_env do
unless system(spawn_script)
message = 'gitaly spawn failed'
message += " (try `rm -rf #{gitaly_dir}` ?)" unless ci?
raise message
raise gitaly_failure_message
end
end
@ -616,6 +614,39 @@ module TestEnv
expected_version == sha.chomp
end
def gitaly_failure_message
message = "gitaly spawn failed\n\n"
message += "- The `gitaly` binary does not exist: #{gitaly_binary}\n" unless File.exist?(gitaly_binary)
message += "- The `praefect` binary does not exist: #{praefect_binary}\n" unless File.exist?(praefect_binary)
message += "- The `git` binary does not exist: #{git_binary}\n" unless File.exist?(git_binary)
message += "\nCheck log/gitaly-test.log for errors.\n"
unless ci?
message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly build git.`\n"
message += "\nOtherwise, try running `rm -rf #{gitaly_dir}`."
end
message
end
def git_binary
File.join(gitaly_dir, "_build", "deps", "git", "install", "bin", "git")
end
def gitaly_binary
File.join(gitaly_dir, "_build", "bin", "gitaly")
end
def praefect_binary
File.join(gitaly_dir, "_build", "bin", "praefect")
end
def git_binary_exists?
File.exist?(git_binary)
end
end
require_relative('../../../ee/spec/support/helpers/ee/test_env') if Gitlab.ee?

View File

@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
_('Planning hierarchy'),
_('Members')
]
},

View File

@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[
award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project
read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]