Commit 78fe6faf authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents f0080541 02acc3f6
<script>
import { debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import { GlSearchBoxByType } from '@gitlab/ui';
import Tracking from '~/tracking';
import frequentItemsMixin from './frequent_items_mixin';
......@@ -9,7 +9,7 @@ const trackingMixin = Tracking.mixin();
export default {
components: {
GlIcon,
GlSearchBoxByType,
},
mixins: [frequentItemsMixin, trackingMixin],
data() {
......@@ -33,22 +33,15 @@ export default {
},
methods: {
...mapActions(['setSearchQuery']),
setFocus() {
this.$refs.search.focus();
},
},
};
</script>
<template>
<div class="search-input-container d-none d-sm-block">
<input
ref="search"
<gl-search-box-by-type
v-model="searchQuery"
:placeholder="translations.searchInputPlaceholder"
type="search"
class="form-control"
/>
<gl-icon v-if="!searchQuery" name="search" class="search-icon" />
</div>
</template>
......@@ -13,7 +13,7 @@ class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
NotificationService.new.new_note(note) unless note.skip_notification?
NotificationService.new.new_note(note) unless note.skip_notification? || note.author.ghost?
Notes::PostProcessService.new(note).execute
else
Gitlab::AppLogger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
......
---
title: Apply new GitLab UI for new trial page
merge_request: 53447
author: Yogi (@yo)
type: other
---
title: Skip new note notifications when author is deleted
merge_request: 53699
author:
type: changed
---
title: Apply new GitLab UI for search in frequent items search
merge_request: 53368
author: Yogi (@yo)
type: other
---
name: ci_custom_yaml_tags
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52104
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300155
milestone: '13.9'
type: development
group: group::pipeline authoring
default_enabled: false
......@@ -90,6 +90,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_methods(Gitlab::Highlight)
instrumentation.instrument_instance_methods(Gitlab::Highlight)
instrumentation.instrument_instance_method(Gitlab::Ci::Config::Yaml::Tags::Resolver, :to_hash)
Gitlab.ee do
instrumentation.instrument_instance_methods(Elastic::Latest::GitInstanceProxy)
......
......@@ -2028,3 +2028,56 @@ See the [troubleshooting documentation](troubleshooting.md).
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Cloud Native Deployment (optional)
Hybrid installations leverage the benefits of both cloud native and traditional
deployments. We recommend shifting the Sidekiq and Webservice components into
Kubernetes to reap cloud native workload management benefits while the others
are deployed using the traditional server method already described.
The following sections detail this hybrid approach.
### Cluster topology
The following table provides a starting point for hybrid
deployment infrastructure. The recommendations use Google Cloud's Kubernetes Engine (GKE)
and associated machine types, but the memory and CPU requirements should
translate to most other providers.
Machine count | Machine type | Allocatable vCPUs | Allocatable memory (GB) | Purpose
-|-|-|-|-
2 | `n1-standard-4` | 7.75 | 25 | Non-GitLab resources, including Grafana, NGINX, and Prometheus
4 | `n1-standard-4` | 15.5 | 50 | GitLab Sidekiq pods
4 | `n1-highcpu-32` | 127.5 | 118 | GitLab Webservice pods
"Allocatable" in this table refers to the amount of resources available to workloads deployed in Kubernetes _after_ accounting for the overhead of running Kubernetes itself.
### Resource usage settings
The following formulas help when calculating how many pods may be deployed within resource constraints.
The [10k reference architecture example values file](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/examples/ref/10k.yaml)
documents how to apply the calculated configuration to the Helm Chart.
#### Sidekiq
Sidekiq pods should generally have 1 vCPU and 2 GB of memory.
[The provided starting point](#cluster-topology) allows the deployment of up to
16 Sidekiq pods. Expand available resources using the 1vCPU to 2GB memory
ratio for each additional pod.
For further information on resource usage, see the [Sidekiq resources](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/#resources).
#### Webservice
Webservice pods typically need about 1 vCPU and 1.25 GB of memory _per worker_.
Each Webservice pod will consume roughly 2 vCPUs and 2.5 GB of memory using
the [recommended topology](#cluster-topology) because two worker processes
are created by default.
The [provided recommendations](#cluster-topology) allow the deployment of up to 28
Webservice pods. Expand available resources using the ratio of 1 vCPU to 1.25 GB of memory
_per each worker process_ for each additional Webservice pod.
For further information on resource usage, see the [Webservice resources](https://docs.gitlab.com/charts/charts/gitlab/webservice/#resources).
......@@ -207,19 +207,41 @@ To add a redirect:
1. Assign the MR to a technical writer for review and merge.
1. If the redirect is to one of the 4 internal docs projects (not an external URL),
create an MR in [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs):
1. Update [`_redirects`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/master/content/_redirects)
1. Update [`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/master/content/_data/redirects.yaml)
with one redirect entry for each renamed or moved file. This code works for
<https://docs.gitlab.com> links only:
<https://docs.gitlab.com> links only. Keep them alphabetically sorted:
```plaintext
/ee/path/to/old_file.html /ee/path/to/new_file 302 # To be removed after YYYY-MM-DD
```yaml
- from: /ee/path/to/old_file.html
to: /ee/path/to/new_file.html
remove_date: YYYY-MM-DD
```
The path must start with the internal project directory `/ee` for `gitlab`,
`/gitlab-runner`, `/omnibus-gitlab` or `charts`, and must end with `.html`.
The path must start with the internal project directory `/ee`,
`/runner`, `/omnibus` or `/charts`, and end with either `.html` or `/`
for a clean URL.
`_redirects` entries can be removed after one year.
If the `from:` redirect is an `index.html` file, add a duplicate entry for
the `/` URL (without `index.html). For example:
```yaml
- from: /ee/user/project/operations/index.html
to: /ee/operations/index.html
remove_date: 2021-11-01
- from: /ee/user/project/operations/
to: /ee/operations/index.html
remove_date: 2021-11-01
```
The `remove_date` should be one year after the redirect is submitted.
1. Run the Rake task in the `gitlab-docs` project to populate the `_redirects` file:
```shell
bundle exec rake redirects
```
1. Add both `content/_redirects` and `content/_data/redirects.yaml` to your MR.
1. Search for links to the old file. You must find and update all links to the old file:
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
......
......@@ -15,31 +15,31 @@
- else
.form-group
= label_tag :first_name, _('First name'), for: :first_name, class: 'col-form-label'
= text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control', required: true
= text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control gl-form-input', required: true
- if experiment_enabled?(:remove_known_trial_form_fields) && current_user.last_name.present?
= hidden_field_tag :last_name, current_user.last_name
- else
.form-group
= label_tag :last_name, _('Last name'), for: :last_name, class: 'col-form-label'
= text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control', required: true
= text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control gl-form-input', required: true
- if experiment_enabled?(:remove_known_trial_form_fields) && current_user.organization.present?
= hidden_field_tag :company_name, current_user.organization
- else
.form-group
= label_tag :company_name, _('Company name'), for: :company_name, class: 'col-form-label'
= text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control', required: true
= text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control gl-form-input', required: true
.form-group.gl-select2-html5-required-fix
= label_tag :company_size, _('Number of employees'), for: :company_size, class: 'col-form-label'
= select_tag :company_size, company_size_options_for_select(params[:company_size]), include_blank: true, class: 'select2', required: true
.form-group
= label_tag :phone_number, _('Telephone number'), for: :phone_number, class: 'col-form-label'
= text_field_tag :phone_number, params[:phone_number], class: 'form-control', required: true
= text_field_tag :phone_number, params[:phone_number], class: 'form-control gl-form-input', required: true
.form-group
= label_tag :number_of_users, _('How many users will be evaluating the trial?'), for: :number_of_users, class: 'col-form-label'
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control', required: true, min: 1
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control gl-form-input', required: true, min: 1
.form-group.gl-select2-html5-required-fix
= label_tag :country, _('Country'), class: 'col-form-label'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: 'select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country]}
= submit_tag _('Continue'), class: 'btn btn-success btn-block'
= submit_tag _('Continue'), class: 'btn gl-button btn-success btn-block'
= render 'skip_trial'
......@@ -13,7 +13,8 @@ module Gitlab
RESCUE_ERRORS = [
Gitlab::Config::Loader::FormatError,
Extendable::ExtensionError,
External::Processor::IncludeError
External::Processor::IncludeError,
Config::Yaml::Tags::TagError
].freeze
attr_reader :root
......@@ -89,6 +90,24 @@ module Gitlab
end
def build_config(config)
if ::Feature.enabled?(:ci_custom_yaml_tags, @context.project, default_enabled: :yaml)
build_config_with_custom_tags(config)
else
build_config_without_custom_tags(config)
end
end
def build_config_with_custom_tags(config)
initial_config = Config::Yaml.load!(config, project: @context.project)
initial_config = Config::External::Processor.new(initial_config, @context).perform
initial_config = Config::Extendable.new(initial_config).to_hash
initial_config = Config::Yaml::Tags::Resolver.new(initial_config).to_hash
initial_config = Config::EdgeStagesInjector.new(initial_config).to_hash
initial_config
end
def build_config_without_custom_tags(config)
initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
initial_config = Config::External::Processor.new(initial_config, @context).perform
initial_config = Config::Extendable.new(initial_config).to_hash
......
......@@ -60,7 +60,11 @@ module Gitlab
def content_hash
strong_memoize(:content_yaml) do
Gitlab::Config::Loader::Yaml.new(content).load!
if ::Feature.enabled?(:ci_custom_yaml_tags, context.project, default_enabled: :yaml)
::Gitlab::Ci::Config::Yaml.load!(content)
else
Gitlab::Config::Loader::Yaml.new(content).load!
end
end
rescue Gitlab::Config::Loader::FormatError
nil
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Yaml
AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
class << self
def load!(content, project: nil)
ensure_custom_tags
Gitlab::Config::Loader::Yaml.new(content, additional_permitted_classes: AVAILABLE_TAGS).load!
end
private
def ensure_custom_tags
@ensure_custom_tags ||= begin
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
true
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Yaml
module Tags
TagError = Class.new(StandardError)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Yaml
module Tags
class Base
CircularReferenceError = Class.new(Tags::TagError)
NotValidError = Class.new(Tags::TagError)
extend ::Gitlab::Utils::Override
attr_accessor :resolved_status, :resolved_value, :data
def self.tag
raise NotImplementedError
end
# Only one of the `seq`, `scalar`, `map` fields is available.
def init_with(coder)
@data = {
tag: coder.tag, # This is the custom YAML tag, like !reference or !flatten
style: coder.style,
seq: coder.seq, # This holds Array data
scalar: coder.scalar, # This holds data of basic types, like String.
map: coder.map # This holds Hash data.
}
end
def valid?
raise NotImplementedError
end
def resolve(resolver)
raise NotValidError, validation_error_message unless valid?
raise CircularReferenceError, circular_error_message if resolving?
return resolved_value if resolved?
self.resolved_status = :in_progress
self.resolved_value = _resolve(resolver)
self.resolved_status = :done
resolved_value
end
private
def _resolve(resolver)
raise NotImplementedError
end
def resolved?
resolved_status == :done
end
def resolving?
resolved_status == :in_progress
end
def circular_error_message
"#{data[:tag]} #{data[:seq].inspect} is part of a circular chain"
end
def validation_error_message
"#{data[:tag]} #{(data[:scalar].presence || data[:map].presence || data[:seq]).inspect} is not valid"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Yaml
module Tags
class Reference < Base
MissingReferenceError = Class.new(Tags::TagError)
def self.tag
'!reference'
end
override :valid?
def valid?
data[:seq].is_a?(Array) &&
!data[:seq].empty? &&
data[:seq].all? { |identifier| identifier.is_a?(String) }
end
private
def location
data[:seq].to_a.map(&:to_sym)
end
override :_resolve
def _resolve(resolver)
object = resolver.config.dig(*location)
value = resolver.deep_resolve(object)
raise MissingReferenceError, missing_ref_error_message unless value
value
end
def missing_ref_error_message
"#{data[:tag]} #{data[:seq].inspect} could not be found"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Yaml
module Tags
# This class is the entry point for transforming custom YAML tags back
# into primitive objects.
# Usage: `Resolver.new(a_hash_including_custom_tag_objects).to_hash`
#
class Resolver
attr_reader :config
def initialize(config)
@config = config.deep_dup
end
def to_hash
deep_resolve(config)
end
def deep_resolve(object)
case object
when Array
object.map(&method(:resolve_wrapper))
when Hash
object.deep_transform_values(&method(:resolve_wrapper))
else
resolve_wrapper(object)
end
end
def resolve_wrapper(object)
if object.respond_to?(:resolve)
object.resolve(self)
else
object
end
end
end
end
end
end
end
end
......@@ -12,8 +12,12 @@ module Gitlab
MAX_YAML_SIZE = 1.megabyte
MAX_YAML_DEPTH = 100
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
def initialize(config, additional_permitted_classes: [])
@config = YAML.safe_load(config,
permitted_classes: [Symbol, *additional_permitted_classes],
permitted_symbols: [],
aliases: true
)
rescue Psych::Exception => e
raise Loader::FormatError, e.message
end
......
import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType } from '@gitlab/ui';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import { createStore } from '~/frequent_items/store';
......@@ -15,6 +16,8 @@ describe('FrequentItemsSearchInputComponent', () => {
propsData: { namespace },
});
const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
beforeEach(() => {
store = createStore({ dropdownType: 'project' });
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
......@@ -32,26 +35,13 @@ describe('FrequentItemsSearchInputComponent', () => {
vm.$destroy();
});
describe('methods', () => {
describe('setFocus', () => {
it('should set focus to search input', () => {
jest.spyOn(vm.$refs.search, 'focus').mockImplementation(() => {});
vm.setFocus();
expect(vm.$refs.search.focus).toHaveBeenCalled();
});
});
});
describe('template', () => {
it('should render component element', () => {
expect(wrapper.classes()).toContain('search-input-container');
expect(wrapper.find('input.form-control').exists()).toBe(true);
expect(wrapper.find('.search-icon').exists()).toBe(true);
expect(wrapper.find('input.form-control').attributes('placeholder')).toBe(
'Search your projects',
);
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().attributes()).toMatchObject({
placeholder: 'Search your projects',
});
});
});
......@@ -62,9 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => {
const value = 'my project';
const input = wrapper.find('input');
input.setValue(value);
input.trigger('input');
findSearchBoxByType().vm.$emit('input', value);
await wrapper.vm.$nextTick();
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference do
let(:config) do
Gitlab::Ci::Config::Yaml.load!(yaml)
end
describe '.tag' do
it 'implements the tag method' do
expect(described_class.tag).to eq('!reference')
end
end
describe '#resolve' do
subject { Gitlab::Ci::Config::Yaml::Tags::Resolver.new(config).to_hash }
context 'with circular references' do
let(:yaml) do
<<~YML
a: !reference [b]
b: !reference [a]
YML
end
it 'raises CircularReferenceError' do
expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] is part of a circular chain'
end
end
context 'with nested circular references' do
let(:yaml) do
<<~YML
a: !reference [b, c]
b: { c: !reference [d, e, f] }
d: { e: { f: !reference [a] } }
YML
end
it 'raises CircularReferenceError' do
expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b", "c"] is part of a circular chain'
end
end
context 'with missing references' do
let(:yaml) { 'a: !reference [b]' }
it 'raises MissingReferenceError' do
expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] could not be found'
end
end
context 'with invalid references' do
using RSpec::Parameterized::TableSyntax
where(:yaml, :error_message) do
'a: !reference' | '!reference [] is not valid'
'a: !reference str' | '!reference "str" is not valid'
'a: !reference 1' | '!reference "1" is not valid'
'a: !reference [1]' | '!reference [1] is not valid'
'a: !reference { b: c }' | '!reference {"b"=>"c"} is not valid'
end
with_them do
it 'raises an error' do
expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, error_message
end
end
end
context 'with arrays' do
let(:yaml) do
<<~YML
a: { b: [1, 2] }
c: { d: { e: [3, 4] } }
f: { g: [ !reference [a, b], 5, !reference [c, d, e]] }
YML
end
it { is_expected.to match(a_hash_including({ f: { g: [[1, 2], 5, [3, 4]] } })) }
end
context 'with hashes' do
context 'when referencing an entire hash' do
let(:yaml) do
<<~YML
a: { b: { c: 'c', d: 'd' } }
e: { f: !reference [a, b] }
YML
end
it { is_expected.to match(a_hash_including({ e: { f: { c: 'c', d: 'd' } } })) }
end
context 'when referencing only a hash value' do
let(:yaml) do
<<~YML
a: { b: { c: 'c', d: 'd' } }
e: { f: { g: !reference [a, b, c], h: 'h' } }
i: !reference [e, f]
YML
end
it { is_expected.to match(a_hash_including({ i: { g: 'c', h: 'h' } })) }
end
context 'when referencing a value before its definition' do
let(:yaml) do
<<~YML
a: { b: !reference [c, d] }
g: { h: { i: 'i', j: 1 } }
c: { d: { e: !reference [g, h, j], f: 'f' } }
YML
end
it { is_expected.to match(a_hash_including({ a: { b: { e: 1, f: 'f' } } })) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver do
let(:config) do
Gitlab::Ci::Config::Yaml.load!(yaml)
end
describe '#to_hash' do
subject { described_class.new(config).to_hash }
context 'when referencing deeply nested arrays' do
let(:yaml_templates) do
<<~YML
.job-1:
script:
- echo doing step 1 of job 1
- echo doing step 2 of job 1
.job-2:
script:
- echo doing step 1 of job 2
- !reference [.job-1, script]
- echo doing step 2 of job 2
.job-3:
script:
- echo doing step 1 of job 3
- !reference [.job-2, script]
- echo doing step 2 of job 3
YML
end
let(:job_yaml) do
<<~YML
test:
script:
- echo preparing to test
- !reference [.job-3, script]
- echo test finished
YML
end
shared_examples 'expands references' do
it 'expands the references' do
is_expected.to match({
'.job-1': {
script: [
'echo doing step 1 of job 1',
'echo doing step 2 of job 1'
]
},
'.job-2': {
script: [
'echo doing step 1 of job 2',
[
'echo doing step 1 of job 1',
'echo doing step 2 of job 1'
],
'echo doing step 2 of job 2'
]
},
'.job-3': {
script: [
'echo doing step 1 of job 3',
[
'echo doing step 1 of job 2',
[
'echo doing step 1 of job 1',
'echo doing step 2 of job 1'
],
'echo doing step 2 of job 2'
],
'echo doing step 2 of job 3'
]
},
test: {
script: [
'echo preparing to test',
[
'echo doing step 1 of job 3',
[
'echo doing step 1 of job 2',
[
'echo doing step 1 of job 1',
'echo doing step 2 of job 1'
],
'echo doing step 2 of job 2'
],
'echo doing step 2 of job 3'
],
'echo test finished'
]
}
})
end
end
context 'when templates are defined before the job' do
let(:yaml) do
<<~YML
#{yaml_templates}
#{job_yaml}
YML
end
it_behaves_like 'expands references'
end
context 'when templates are defined after the job' do
let(:yaml) do
<<~YML
#{job_yaml}
#{yaml_templates}
YML
end
it_behaves_like 'expands references'
end
end
end
end
......@@ -263,6 +263,26 @@ RSpec.describe Gitlab::Ci::Config do
end
end
end
context 'when yaml uses circular !reference' do
let(:yml) do
<<~YAML
job-1:
script:
- !reference [job-2, before_script]
job-2:
before_script: !reference [job-1, script]
YAML
end
it 'raises error' do
expect { config }.to raise_error(
described_class::ConfigError,
/\!reference \["job-2", "before_script"\] is part of a circular chain/
)
end
end
end
context "when using 'include' directive" do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe '!reference tags' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
before do
stub_ci_pipeline_yaml_file(config)
end
context 'with valid config' do
let(:config) do
<<~YAML
.job-1:
script:
- echo doing step 1 of job 1
.job-2:
before_script:
- ls
script: !reference [.job-1, script]
job:
before_script: !reference [.job-2, before_script]
script:
- echo doing my first step
- !reference [.job-2, script]
- echo doing my last step
YAML
end
it 'creates a pipeline' do
expect(pipeline).to be_persisted
expect(pipeline.builds.first.options).to match(a_hash_including({
'before_script' => ['ls'],
'script' => [
'echo doing my first step',
'echo doing step 1 of job 1',
'echo doing my last step'
]
}))
end
end
context 'with invalid config' do
let(:config) do
<<~YAML
job-1:
script:
- echo doing step 1 of job 1
- !reference [job-3, script]
job-2:
script:
- echo doing step 1 of job 2
- !reference [job-3, script]
job-3:
script:
- echo doing step 1 of job 3
- !reference [job-1, script]
YAML
end
it 'creates a pipeline without builds' do
expect(pipeline).to be_persisted
expect(pipeline.builds).to be_empty
expect(pipeline.yaml_errors).to eq("!reference [\"job-3\", \"script\"] is part of a circular chain")
end
end
end
end
......@@ -65,4 +65,14 @@ RSpec.describe NewNoteWorker do
subject.perform(note.id)
end
end
context 'when Note author has been deleted' do
let_it_be(:note) { create(:note, author: User.ghost) }
it "does not call NotificationService" do
expect(NotificationService).not_to receive(:new)
described_class.new.perform(note.id)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment