Commit a2513470 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 4ee20b8e 8a5d5b40
This diff is collapsed.
......@@ -204,8 +204,13 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
# 'rspec:linux: [aws, max memory]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
else
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
end
common_name.strip!
common_name
......
......@@ -1384,8 +1384,6 @@ class MergeRequest < ApplicationRecord
end
def has_coverage_reports?
return false unless Feature.enabled?(:coverage_report_view, project, default_enabled: true)
actual_head_pipeline&.has_coverage_reports?
end
......
......@@ -3,7 +3,6 @@ module Ci
module Pipelines
class CreateArtifactService
def execute(pipeline)
return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project)
return unless pipeline.can_generate_coverage_reports?
return if pipeline.has_coverage_reports?
......
......@@ -36,5 +36,3 @@
.form-actions
= f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
= link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
---
title: Remove coverage_report_view feature flag
merge_request: 43711
author: David Barr @davebarr
type: removed
---
title: Add internal API to download LFS objects
merge_request: 42161
author:
type: added
---
name: coverage_report_view
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21791
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/211410
group: 'group::verify testing'
name: one_dimensional_matrix
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42170
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/256062
type: development
default_enabled: true
group: group::pipeline authoring
default_enabled: false
......@@ -3572,6 +3572,11 @@ job split into three separate jobs.
Use `matrix:` to configure different variables for jobs that are running in parallel.
There can be from 2 to 50 jobs.
In GitLab 13.5 and later, you can have one-dimensional matrices with a single job.
The ability to have one-dimensional matrices is [deployed behind a feature flag](../../user/feature_flags.md),
disabled by default. It's disabled on GitLab.com. To use it in a GitLab self-managed
instance, ask a GitLab administrator to [enable the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE-ONLY)**
Every job gets the same `CI_NODE_TOTAL` [environment variable](../variables/README.md#predefined-environment-variables) value, and a unique `CI_NODE_INDEX` value.
```yaml
......
......@@ -8,10 +8,7 @@ type: reference, howto
# Test Coverage Visualization
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
> - [Feature flag enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/211410) in GitLab 13.4.
> - It's enabled on GitLab.com.
> - It can be disabled per-project.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-code-coverage-visualization). **(CORE ONLY)**
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/249811) in GitLab 13.5.
With the help of [GitLab CI/CD](../../../ci/README.md), you can collect the test
coverage information of your favorite testing or coverage-analysis tool, and visualize
......@@ -151,27 +148,3 @@ coverage-jdk11:
reports:
cobertura: build/cobertura.xml
```
## Enable or disable code coverage visualization
This feature comes with the `:coverage_report_view` feature flag enabled by
default. It is enabled on GitLab.com. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can disable it for your instance. Test coverage visualization can be enabled or disabled per-project.
To disable it:
```ruby
# Instance-wide
Feature.disable(:coverage_report_view)
# or by project
Feature.disable(:coverage_report_view, Project.find(<project id>))
```
To enable it:
```ruby
# Instance-wide
Feature.enable(:coverage_report_view)
# or by project
Feature.enable(:coverage_report_view, Project.find(<project id>))
```
- group = local_assigns.fetch(:group)
- return unless Gitlab::Auth::Ldap::Config.group_sync_enabled? && group.persisted?
%h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: group
= render 'ldap_group_links/ldap_group_links', group: group
......@@ -249,6 +249,7 @@ module API
end
mount ::API::Internal::Base
mount ::API::Internal::Lfs
mount ::API::Internal::Pages
mount ::API::Internal::Kubernetes
......
# frozen_string_literal: true
module API
module Internal
class Lfs < Grape::API::Instance
use Rack::Sendfile
before { authenticate_by_gitlab_shell_token! }
helpers do
def find_lfs_object(lfs_oid)
LfsObject.find_by_oid(lfs_oid)
end
end
namespace 'internal' do
namespace 'lfs' do
desc 'Get LFS URL for object ID' do
detail 'This feature was introduced in GitLab 13.5.'
end
params do
requires :oid, type: String, desc: 'The object ID to query'
requires :gl_repository, type: String, desc: "Project identifier (e.g. project-1)"
end
get "/" do
lfs_object = find_lfs_object(params[:oid])
not_found! unless lfs_object
_, project, repo_type = Gitlab::GlRepository.parse(params[:gl_repository])
not_found! unless repo_type.project? && project
not_found! unless lfs_object.project_allowed_access?(project)
file = lfs_object.file
not_found! unless file&.exists?
content_type 'application/octet-stream'
if file.file_storage?
sendfile file.path
else
workhorse_headers = Gitlab::Workhorse.send_url(file.url)
header workhorse_headers[0], workhorse_headers[1]
env['api.format'] = :binary
body nil
end
end
end
end
end
end
end
......@@ -14,7 +14,7 @@ module Gitlab
validations do
validates :config, variables: { array_values: true }
validates :config, length: {
minimum: 2,
minimum: :minimum,
too_short: 'requires at least %{count} items'
}
end
......@@ -28,6 +28,10 @@ module Gitlab
.map { |key, value| [key.to_s, Array(value).map(&:to_s)] }
.to_h
end
def minimum
::Gitlab::Ci::Features.one_dimensional_matrix_enabled? ? 1 : 2
end
end
end
end
......
......@@ -46,10 +46,6 @@ module Gitlab
Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
end
def self.coverage_report_view?(project)
::Feature.enabled?(:coverage_report_view, project, default_enabled: true)
end
def self.child_of_child_pipeline_enabled?(project)
::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true)
end
......@@ -66,6 +62,10 @@ module Gitlab
def self.new_artifact_file_reader_enabled?(project)
::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: true)
end
def self.one_dimensional_matrix_enabled?
::Feature.enabled?(:one_dimensional_matrix, default_enabled: false)
end
end
end
end
......@@ -109,8 +109,8 @@ RSpec.describe InvitesController, :snowplow do
it 'tracks the user as experiment group' do
request
expect_snowplow_event(snowplow_event.merge(action: 'opened'))
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
expect_snowplow_event(**snowplow_event.merge(action: 'opened'))
expect_snowplow_event(**snowplow_event.merge(action: 'accepted'))
end
end
......@@ -121,8 +121,8 @@ RSpec.describe InvitesController, :snowplow do
it 'tracks the user as control group' do
request
expect_snowplow_event(snowplow_event.merge(action: 'opened'))
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
expect_snowplow_event(**snowplow_event.merge(action: 'opened'))
expect_snowplow_event(**snowplow_event.merge(action: 'accepted'))
end
end
......@@ -173,7 +173,7 @@ RSpec.describe InvitesController, :snowplow do
it 'tracks the user as experiment group' do
request
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
expect_snowplow_event(**snowplow_event.merge(action: 'accepted'))
end
end
......@@ -184,7 +184,7 @@ RSpec.describe InvitesController, :snowplow do
it 'tracks the user as control group' do
request
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
expect_snowplow_event(**snowplow_event.merge(action: 'accepted'))
end
end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'spec_helper'
require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
......@@ -46,33 +46,140 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
end
end
context 'when entry config has only one variable' do
let(:config) do
[
{
'VAR_1' => %w[test]
}
]
context 'with one_dimensional_matrix feature flag enabled' do
before do
stub_feature_flags(one_dimensional_matrix: true)
matrix.compose!
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
context 'when entry config has only one variable with multiple values' do
let(:config) do
[
{
'VAR_1' => %w[build test]
}
]
end
describe '#errors' do
it 'returns error about too many jobs' do
expect(matrix.errors)
.to include('variables config requires at least 2 items')
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to be_empty
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
end
end
context 'when entry config has only one variable with one value' do
let(:config) do
[
{
'VAR_1' => %w[test]
}
]
end
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to be_empty
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
end
end
end
end
end
context 'with one_dimensional_matrix feature flag disabled' do
before do
stub_feature_flags(one_dimensional_matrix: false)
matrix.compose!
end
describe '#value' do
before do
matrix.compose!
context 'when entry config has only one variable with multiple values' do
let(:config) do
[
{
'VAR_1' => %w[build test]
}
]
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => ['test'] }])
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about too many jobs' do
expect(matrix.errors)
.to include('variables config requires at least 2 items')
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
end
end
context 'when entry config has only one variable with one value' do
let(:config) do
[
{
'VAR_1' => %w[test]
}
]
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to include('variables config requires at least 2 items')
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
end
end
end
end
end
......
# frozen_string_literal: true
require 'fast_spec_helper'
# After Feature one_dimensional_matrix is removed, this can be changed back to fast_spec_helper
require 'spec_helper'
require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
......@@ -45,43 +46,71 @@ RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
end
end
context 'when entry value is not correct' do
shared_examples 'invalid variables' do |message|
describe '#errors' do
it 'saves errors' do
expect(entry.errors).to include(message)
end
context 'with one_dimensional_matrix feature flag enabled' do
context 'with only one variable' do
before do
stub_feature_flags(one_dimensional_matrix: true)
end
let(:config) { { VAR: 'test' } }
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
end
end
end
context 'with array' do
let(:config) { [:VAR, 'test'] }
context 'with one_dimensional_matrix feature flag disabled' do
context 'when entry value is not correct' do
before do
stub_feature_flags(one_dimensional_matrix: false)
end
shared_examples 'invalid variables' do |message|
describe '#errors' do
it 'saves errors' do
expect(entry.errors).to include(message)
end
end
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
context 'with empty array' do
let(:config) { { VAR: 'test', VAR2: [] } }
context 'with array' do
let(:config) { [:VAR, 'test'] }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
context 'with nested array' do
let(:config) { { VAR: 'test', VAR2: [1, [2]] } }
context 'with empty array' do
let(:config) { { VAR: 'test', VAR2: [] } }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
context 'with only one variable' do
let(:config) { { VAR: 'test' } }
context 'with nested array' do
let(:config) { { VAR: 'test', VAR2: [1, [2]] } }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
it_behaves_like 'invalid variables', /variables config requires at least 2 items/
context 'with one_dimensional_matrix feature flag disabled' do
context 'with only one variable' do
let(:config) { { VAR: 'test' } }
it_behaves_like 'invalid variables', /variables config requires at least 2 items/
end
end
end
end
end
......
......@@ -493,47 +493,104 @@ RSpec.describe CommitStatus do
end
end
describe '#group_name' do
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
subject { commit_status.group_name }
context 'with the one_dimensional_matrix feature flag disabled' do
describe '#group_name' do
before do
stub_feature_flags(one_dimensional_matrix: false)
end
tests = {
'rspec:windows' => 'rspec:windows',
'rspec:windows 0' => 'rspec:windows 0',
'rspec:windows 0 test' => 'rspec:windows 0 test',
'rspec:windows 0 1' => 'rspec:windows',
'rspec:windows 0 1 name' => 'rspec:windows name',
'rspec:windows 0/1' => 'rspec:windows',
'rspec:windows 0/1 name' => 'rspec:windows name',
'rspec:windows 0:1' => 'rspec:windows',
'rspec:windows 0:1 name' => 'rspec:windows name',
'rspec:windows 10000 20000' => 'rspec:windows',
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
'0 :/ 1 name ruby' => 'name ruby',
'rspec: [aws]' => 'rspec: [aws]',
'rspec: [aws] 0/1' => 'rspec: [aws]',
'rspec: [aws, max memory]' => 'rspec',
'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
'rspec: [inception: [something, other thing], value]' => 'rspec',
'rspec:windows 0/1: [name, other]' => 'rspec:windows',
'rspec:windows: [name, other] 0/1' => 'rspec:windows',
'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
'rspec:windows: [0/1, name]' => 'rspec:windows',
'rspec:windows: [, ]' => 'rspec:windows',
'rspec:windows: [name]' => 'rspec:windows: [name]',
'rspec:windows: [name,other]' => 'rspec:windows: [name,other]'
}
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
subject { commit_status.group_name }
tests = {
'rspec:windows' => 'rspec:windows',
'rspec:windows 0' => 'rspec:windows 0',
'rspec:windows 0 test' => 'rspec:windows 0 test',
'rspec:windows 0 1' => 'rspec:windows',
'rspec:windows 0 1 name' => 'rspec:windows name',
'rspec:windows 0/1' => 'rspec:windows',
'rspec:windows 0/1 name' => 'rspec:windows name',
'rspec:windows 0:1' => 'rspec:windows',
'rspec:windows 0:1 name' => 'rspec:windows name',
'rspec:windows 10000 20000' => 'rspec:windows',
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
'0 :/ 1 name ruby' => 'name ruby',
'rspec: [aws]' => 'rspec: [aws]',
'rspec: [aws] 0/1' => 'rspec: [aws]',
'rspec: [aws, max memory]' => 'rspec',
'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
'rspec: [inception: [something, other thing], value]' => 'rspec',
'rspec:windows 0/1: [name, other]' => 'rspec:windows',
'rspec:windows: [name, other] 0/1' => 'rspec:windows',
'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
'rspec:windows: [0/1, name]' => 'rspec:windows',
'rspec:windows: [, ]' => 'rspec:windows',
'rspec:windows: [name]' => 'rspec:windows: [name]',
'rspec:windows: [name,other]' => 'rspec:windows: [name,other]'
}
tests.each do |name, group_name|
it "'#{name}' puts in '#{group_name}'" do
commit_status.name = name
is_expected.to eq(group_name)
end
end
end
end
tests.each do |name, group_name|
it "'#{name}' puts in '#{group_name}'" do
commit_status.name = name
context 'with one_dimensional_matrix feature flag enabled' do
describe '#group_name' do
before do
stub_feature_flags(one_dimensional_matrix: true)
end
is_expected.to eq(group_name)
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
subject { commit_status.group_name }
tests = {
'rspec:windows' => 'rspec:windows',
'rspec:windows 0' => 'rspec:windows 0',
'rspec:windows 0 test' => 'rspec:windows 0 test',
'rspec:windows 0 1' => 'rspec:windows',
'rspec:windows 0 1 name' => 'rspec:windows name',
'rspec:windows 0/1' => 'rspec:windows',
'rspec:windows 0/1 name' => 'rspec:windows name',
'rspec:windows 0:1' => 'rspec:windows',
'rspec:windows 0:1 name' => 'rspec:windows name',
'rspec:windows 10000 20000' => 'rspec:windows',
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
'0 :/ 1 name ruby' => 'name ruby',
'rspec: [aws]' => 'rspec',
'rspec: [aws] 0/1' => 'rspec',
'rspec: [aws, max memory]' => 'rspec',
'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
'rspec: [inception: [something, other thing], value]' => 'rspec',
'rspec:windows 0/1: [name, other]' => 'rspec:windows',
'rspec:windows: [name, other] 0/1' => 'rspec:windows',
'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
'rspec:windows: [0/1, name]' => 'rspec:windows',
'rspec:windows: [, ]' => 'rspec:windows',
'rspec:windows: [name]' => 'rspec:windows',
'rspec:windows: [name,other]' => 'rspec:windows'
}
tests.each do |name, group_name|
it "'#{name}' puts in '#{group_name}'" do
commit_status.name = name
is_expected.to eq(group_name)
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Internal::Lfs do
include APIInternalBaseHelpers
let_it_be(:project) { create(:project) }
let_it_be(:lfs_object) { create(:lfs_object, :with_file) }
let_it_be(:lfs_objects_project) { create(:lfs_objects_project, project: project, lfs_object: lfs_object) }
let_it_be(:gl_repository) { "project-#{project.id}" }
let_it_be(:filename) { lfs_object.file.path }
let(:secret_token) { Gitlab::Shell.secret_token }
describe 'GET /internal/lfs' do
let(:valid_params) do
{ oid: lfs_object.oid, gl_repository: gl_repository, secret_token: secret_token }
end
context 'with invalid auth' do
let(:invalid_params) { valid_params.merge!(secret_token: 'invalid_tokne') }
it 'returns 401' do
get api("/internal/lfs"), params: invalid_params
end
end
context 'with valid auth' do
context 'LFS in local storage' do
it 'sends the file' do
get api("/internal/lfs"), params: valid_params
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
expect(response.headers['Content-Length'].to_i).to eq(File.stat(filename).size)
expect(response.body).to eq(File.open(filename, 'rb', &:read))
end
# https://www.rubydoc.info/github/rack/rack/master/Rack/Sendfile
it 'delegates sending to Web server' do
get api("/internal/lfs"), params: valid_params, env: { 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('application/octet-stream')
expect(response.headers['Content-Length'].to_i).to eq(0)
expect(response.headers['X-Sendfile']).to be_present
expect(response.body).to eq("")
end
it 'retuns 404 for unknown file' do
params = valid_params.merge(oid: SecureRandom.hex)
get api("/internal/lfs"), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 404 if LFS object does not belong to project' do
other_lfs = create(:lfs_object, :with_file)
params = valid_params.merge(oid: other_lfs.oid)
get api("/internal/lfs"), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'LFS in object storage' do
let!(:lfs_object2) { create(:lfs_object, :with_file) }
let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }
let(:valid_params) do
{ oid: lfs_object2.oid, gl_repository: gl_repository, secret_token: secret_token }
end
before do
stub_lfs_object_storage(enabled: true)
lfs_object2.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'notifies Workhorse to send the file' do
get api("/internal/lfs"), params: valid_params
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
expect(response.headers['Content-Type']).to eq('application/octet-stream')
expect(response.headers['Content-Length'].to_i).to eq(0)
expect(response.body).to eq("")
end
end
end
end
end
......@@ -35,16 +35,6 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do
end
end
context 'when feature is disabled' do
it 'does not create a pipeline artifact' do
stub_feature_flags(coverage_report_view: false)
subject
expect(Ci::PipelineArtifact.count).to eq(0)
end
end
context 'when pipeline artifact has already been created' do
it 'do not raise an error and do not persist the same artifact twice' do
expect { 2.times { described_class.new.execute(pipeline) } }.not_to raise_error(ActiveRecord::RecordNotUnique)
......
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