Commit 2513d32a authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents b5af7529 19ce8b0e
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click'; import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants'; import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
const CUSTOM_APP_WARNING_TEXT = { const CUSTOM_APP_WARNING_TEXT = {
[HELM]: s__(
'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored.',
),
[INGRESS]: s__( [INGRESS]: s__(
'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.', 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
), ),
......
...@@ -504,8 +504,9 @@ module Ci ...@@ -504,8 +504,9 @@ module Ci
return [] unless config_processor return [] unless config_processor
strong_memoize(:stage_seeds) do strong_memoize(:stage_seeds) do
seeds = config_processor.stages_attributes.map do |attributes| seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes|
Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes) seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages)
previous_stages + [seed]
end end
seeds.select(&:included?) seeds.select(&:included?)
......
...@@ -14,6 +14,7 @@ module Clusters ...@@ -14,6 +14,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationStatus
include ::Gitlab::Utils::StrongMemoize
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
...@@ -29,11 +30,22 @@ module Clusters ...@@ -29,11 +30,22 @@ module Clusters
self.status = 'installable' if cluster&.platform_kubernetes_active? self.status = 'installable' if cluster&.platform_kubernetes_active?
end end
# We will implement this in future MRs. # It can only be uninstalled if there are no other applications installed
# Basically we need to check all other applications are not installed # or with intermitent installation statuses in the database.
# first.
def allowed_to_uninstall? def allowed_to_uninstall?
false strong_memoize(:allowed_to_uninstall) do
applications = nil
Clusters::Cluster::APPLICATIONS.each do |application_name, klass|
next if application_name == 'helm'
extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id))
applications = applications.present? ? applications.or(extra_apps) : extra_apps
end
!applications.exists?
end
end end
def install_command def install_command
...@@ -44,6 +56,14 @@ module Clusters ...@@ -44,6 +56,14 @@ module Clusters
) )
end end
def uninstall_command
Gitlab::Kubernetes::Helm::ResetCommand.new(
name: name,
files: files,
rbac: cluster.platform_kubernetes_rbac?
)
end
def has_ssl? def has_ssl?
ca_key.present? && ca_cert.present? ca_key.present? && ca_cert.present?
end end
......
---
title: Allow Helm to be uninstalled from the UI
merge_request: 27359
author:
type: added
...@@ -972,22 +972,28 @@ Parameters: ...@@ -972,22 +972,28 @@ Parameters:
| `channel` | string | false | Default channel to use if others are not configured | | `channel` | string | false | Default channel to use if others are not configured |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines | | `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
| `notify_only_default_branch` | boolean | false | Send notifications only for the default branch | | `notify_only_default_branch` | boolean | false | Send notifications only for the default branch |
| `push_events` | boolean | false | Enable notifications for push events | | `commit_events` | boolean | false | Enable notifications for commit events |
| `issues_events` | boolean | false | Enable notifications for issue events | | `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events | | `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
| `confidential_note_channel` | string | false | The name of the channel to receive confidential note events notifications |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `deployment_channel` | string | false | The name of the channel to receive deployment events notifications |
| `deployment_events` | boolean | false | Enable notifications for deployment events |
| `issue_channel` | string | false | The name of the channel to receive issues events notifications |
| `issues_events` | boolean | false | Enable notifications for issue events |
| `job_events` | boolean | false | Enable notifications for job events |
| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events | | `note_channel` | string | false | The name of the channel to receive note events notifications |
| `note_events` | boolean | false | Enable notifications for note events | | `note_events` | boolean | false | Enable notifications for note events |
| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events | | `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
| `push_channel` | string | false | The name of the channel to receive push events notifications | | `push_channel` | string | false | The name of the channel to receive push events notifications |
| `issue_channel` | string | false | The name of the channel to receive issues events notifications | | `push_events` | boolean | false | Enable notifications for push events |
| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
| `note_channel` | string | false | The name of the channel to receive note events notifications |
| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications | | `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications | | `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications | | `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Slack service ### Delete Slack service
......
...@@ -253,6 +253,7 @@ The applications below can be uninstalled. ...@@ -253,6 +253,7 @@ The applications below can be uninstalled.
| Application | GitLab version | Notes | | Application | GitLab version | Notes |
| ----------- | -------------- | ----- | | ----------- | -------------- | ----- |
| GitLab Runner | 12.2+ | Any running pipelines will be canceled. | | GitLab Runner | 12.2+ | Any running pipelines will be canceled. |
| Helm | 12.2+ | The associated Tiller pod will be deleted and cannot be restored. |
| Ingress | 12.1+ | The associated load balancer and IP will be deleted and cannot be restored. Furthermore, it can only be uninstalled if JupyterHub is not installed. | | Ingress | 12.1+ | The associated load balancer and IP will be deleted and cannot be restored. Furthermore, it can only be uninstalled if JupyterHub is not installed. |
| JupyterHub | 12.1+ | All data not committed to GitLab will be deleted and cannot be restored. | | JupyterHub | 12.1+ | All data not committed to GitLab will be deleted and cannot be restored. |
| Knative | 12.1+ | The associated IP will be deleted and cannot be restored. | | Knative | 12.1+ | The associated IP will be deleted and cannot be restored. |
......
...@@ -9,9 +9,10 @@ module Gitlab ...@@ -9,9 +9,10 @@ module Gitlab
delegate :dig, to: :@attributes delegate :dig, to: :@attributes
def initialize(pipeline, attributes) def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline @pipeline = pipeline
@attributes = attributes @attributes = attributes
@previous_stages = previous_stages
@only = Gitlab::Ci::Build::Policy @only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only)) .fabricate(attributes.delete(:only))
...@@ -19,10 +20,15 @@ module Gitlab ...@@ -19,10 +20,15 @@ module Gitlab
.fabricate(attributes.delete(:except)) .fabricate(attributes.delete(:except))
end end
def name
dig(:name)
end
def included? def included?
strong_memoize(:inclusion) do strong_memoize(:inclusion) do
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) } && all_of_only? &&
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) } none_of_except? &&
all_of_needs?
end end
end end
...@@ -42,6 +48,25 @@ module Gitlab ...@@ -42,6 +48,25 @@ module Gitlab
@attributes.to_h.dig(:options, :trigger).present? @attributes.to_h.dig(:options, :trigger).present?
end end
def all_of_only?
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
end
def none_of_except?
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
end
def all_of_needs?
return true unless Feature.enabled?(:ci_dag_support, @pipeline.project)
return true if dig(:needs_attributes).nil?
dig(:needs_attributes).all? do |need|
@previous_stages.any? do |stage|
stage.seeds_names.include?(need[:name])
end
end
end
def to_resource def to_resource
strong_memoize(:resource) do strong_memoize(:resource) do
if bridge? if bridge?
......
...@@ -10,12 +10,13 @@ module Gitlab ...@@ -10,12 +10,13 @@ module Gitlab
delegate :size, to: :seeds delegate :size, to: :seeds
delegate :dig, to: :seeds delegate :dig, to: :seeds
def initialize(pipeline, attributes) def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline @pipeline = pipeline
@attributes = attributes @attributes = attributes
@previous_stages = previous_stages
@builds = attributes.fetch(:builds).map do |attributes| @builds = attributes.fetch(:builds).map do |attributes|
Seed::Build.new(@pipeline, attributes) Seed::Build.new(@pipeline, attributes, previous_stages)
end end
end end
...@@ -32,6 +33,12 @@ module Gitlab ...@@ -32,6 +33,12 @@ module Gitlab
end end
end end
def seeds_names
strong_memoize(:seeds_names) do
seeds.map(&:name).to_set
end
end
def included? def included?
seeds.any? seeds.any?
end end
...@@ -39,13 +46,7 @@ module Gitlab ...@@ -39,13 +46,7 @@ module Gitlab
def to_resource def to_resource
strong_memoize(:stage) do strong_memoize(:stage) do
::Ci::Stage.new(attributes).tap do |stage| ::Ci::Stage.new(attributes).tap do |stage|
seeds.each do |seed| stage.statuses = seeds.map(&:to_resource)
if seed.bridge?
stage.bridges << seed.to_resource
else
stage.builds << seed.to_resource
end
end
end end
end end
end end
......
...@@ -43,17 +43,6 @@ module Gitlab ...@@ -43,17 +43,6 @@ module Gitlab
command.shelljoin command.shelljoin
end end
def optional_tls_flags
return [] unless files.key?(:'ca.pem')
[
'--tls',
'--tls-ca-cert', "#{files_dir}/ca.pem",
'--tls-cert', "#{files_dir}/cert.pem",
'--tls-key', "#{files_dir}/key.pem"
]
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Kubernetes
module Helm
class ResetCommand
include BaseCommand
include ClientCommand
attr_reader :name, :files
def initialize(name:, rbac:, files:)
@name = name
@files = files
@rbac = rbac
end
def generate_script
super + [
reset_helm_command,
delete_tiller_replicaset
].join("\n")
end
def rbac?
@rbac
end
def pod_name
"uninstall-#{name}"
end
private
# This method can be delete once we upgrade Helm to > 12.13.0
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27096#note_159695900
#
# Tracking this method to be removed here:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/52791#note_199374155
def delete_tiller_replicaset
command = %w[kubectl delete replicaset -n gitlab-managed-apps -l name=tiller]
command.shelljoin
end
def reset_helm_command
command = %w[helm reset] + optional_tls_flags
command.shelljoin
end
end
end
end
end
...@@ -3524,6 +3524,9 @@ msgstr "" ...@@ -3524,6 +3524,9 @@ msgstr ""
msgid "ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications." msgid "ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications."
msgstr "" msgstr ""
msgid "ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The associated certifcate will be deleted and cannot be restored." msgid "ClusterIntegration|The associated certifcate will be deleted and cannot be restored."
msgstr "" msgstr ""
......
...@@ -63,7 +63,8 @@ describe 'Clusters Applications', :js do ...@@ -63,7 +63,8 @@ describe 'Clusters Applications', :js do
Clusters::Cluster.last.application_helm.make_installed! Clusters::Cluster.last.application_helm.make_installed!
expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed') expect(page).not_to have_css('.js-cluster-application-install-button')
expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
end end
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
......
...@@ -38,8 +38,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ...@@ -38,8 +38,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'populates pipeline with stages' do it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted expect(pipeline.stages.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one expect(pipeline.stages.first.statuses).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted expect(pipeline.stages.first.statuses.first).not_to be_persisted
end end
it 'correctly assigns user' do it 'correctly assigns user' do
...@@ -191,8 +191,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ...@@ -191,8 +191,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
step.perform! step.perform!
expect(pipeline.stages.size).to eq 1 expect(pipeline.stages.size).to eq 1
expect(pipeline.stages.first.builds.size).to eq 1 expect(pipeline.stages.first.statuses.size).to eq 1
expect(pipeline.stages.first.builds.first.name).to eq 'rspec' expect(pipeline.stages.first.statuses.first.name).to eq 'rspec'
end end
end end
......
...@@ -6,8 +6,9 @@ describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -6,8 +6,9 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:attributes) { { name: 'rspec', ref: 'master' } } let(:attributes) { { name: 'rspec', ref: 'master' } }
let(:previous_stages) { [] }
let(:seed_build) { described_class.new(pipeline, attributes) } let(:seed_build) { described_class.new(pipeline, attributes, previous_stages) }
describe '#attributes' do describe '#attributes' do
subject { seed_build.attributes } subject { seed_build.attributes }
...@@ -381,4 +382,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -381,4 +382,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end end
end end
end end
describe 'applying needs: dependency' do
subject { seed_build }
let(:attributes) do
{
name: 'rspec',
needs_attributes: [{
name: 'build'
}]
}
end
context 'when build job is not present in prior stages' do
it { is_expected.not_to be_included }
end
context 'when build job is part of prior stages' do
let(:stage_attributes) do
{
name: 'build',
index: 0,
builds: [{ name: 'build' }]
}
end
let(:stage_seed) do
Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, stage_attributes, [])
end
let(:previous_stages) { [stage_seed] }
it { is_expected.to be_included }
end
end
end end
...@@ -5,6 +5,7 @@ require 'spec_helper' ...@@ -5,6 +5,7 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Stage do describe Gitlab::Ci::Pipeline::Seed::Stage do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:previous_stages) { [] }
let(:attributes) do let(:attributes) do
{ name: 'test', { name: 'test',
...@@ -15,7 +16,7 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do ...@@ -15,7 +16,7 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end end
subject do subject do
described_class.new(pipeline, attributes) described_class.new(pipeline, attributes, previous_stages)
end end
describe '#size' do describe '#size' do
...@@ -109,6 +110,17 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do ...@@ -109,6 +110,17 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end end
end end
describe '#seeds_names' do
it 'returns all job names' do
expect(subject.seeds_names).to contain_exactly(
'rspec', 'spinach')
end
it 'returns a set' do
expect(subject.seeds_names).to be_a(Set)
end
end
describe '#to_resource' do describe '#to_resource' do
it 'builds a valid stage object with all builds' do it 'builds a valid stage object with all builds' do
subject.to_resource.save! subject.to_resource.save!
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::ResetCommand do
let(:rbac) { true }
let(:name) { 'helm' }
let(:files) { {} }
let(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
subject { reset_command }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm reset
kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
EOS
end
end
context 'when there is a ca.pem file' do
let(:files) { { 'ca.pem': 'some file content' } }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS1.squish + "\n" + <<~EOS2
helm reset
--tls
--tls-ca-cert /data/helm/helm/config/ca.pem
--tls-cert /data/helm/helm/config/cert.pem
--tls-key /data/helm/helm/config/key.pem
EOS1
kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller
EOS2
end
end
end
describe '#pod_resource' do
subject { reset_command.pod_resource }
context 'rbac is enabled' do
let(:rbac) { true }
it 'generates a pod that uses the tiller serviceAccountName' do
expect(subject.spec.serviceAccountName).to eq('tiller')
end
end
context 'rbac is not enabled' do
let(:rbac) { false }
it 'generates a pod that uses the default serviceAccountName' do
expect(subject.spec.serviceAcccountName).to be_nil
end
end
end
describe '#pod_name' do
subject { reset_command.pod_name }
it { is_expected.to eq('uninstall-helm') }
end
end
...@@ -19,11 +19,27 @@ describe Clusters::Applications::Helm do ...@@ -19,11 +19,27 @@ describe Clusters::Applications::Helm do
end end
describe '#can_uninstall?' do describe '#can_uninstall?' do
let(:helm) { create(:clusters_applications_helm) } context "with other existing applications" do
Clusters::Cluster::APPLICATIONS.keys.each do |application_name|
next if application_name == 'helm'
it do
cluster_application = create("clusters_applications_#{application_name}".to_sym)
helm = cluster_application.cluster.application_helm
expect(helm.allowed_to_uninstall?).to be_falsy
end
end
end
context "without other existing applications" do
subject { helm.can_uninstall? } subject { helm.can_uninstall? }
it { is_expected.to be_falsey } let(:helm) { create(:clusters_applications_helm) }
it { is_expected.to be_truthy }
end
end end
describe '#issue_client_cert' do describe '#issue_client_cert' do
...@@ -73,4 +89,41 @@ describe Clusters::Applications::Helm do ...@@ -73,4 +89,41 @@ describe Clusters::Applications::Helm do
end end
end end
end end
describe '#uninstall_command' do
let(:helm) { create(:clusters_applications_helm) }
subject { helm.uninstall_command }
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::ResetCommand) }
it 'has name' do
expect(subject.name).to eq('helm')
end
it 'has cert files' do
expect(subject.files[:'ca.pem']).to be_present
expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
expect(subject.files[:'cert.pem']).to be_present
expect(subject.files[:'key.pem']).to be_present
cert = OpenSSL::X509::Certificate.new(subject.files[:'cert.pem'])
expect(cert.not_after).to be > 999.years.from_now
end
describe 'rbac' do
context 'rbac cluster' do
it { expect(subject).to be_rbac }
end
context 'non rbac cluster' do
before do
helm.cluster.platform_kubernetes.abac!
end
it { expect(subject).not_to be_rbac }
end
end
end
end end
...@@ -22,7 +22,7 @@ describe ClusterApplicationEntity do ...@@ -22,7 +22,7 @@ describe ClusterApplicationEntity do
end end
it 'has can_uninstall' do it 'has can_uninstall' do
expect(subject[:can_uninstall]).to be_falsey expect(subject[:can_uninstall]).to be_truthy
end end
context 'non-helm application' do context 'non-helm application' do
......
...@@ -1099,6 +1099,62 @@ describe Ci::CreatePipelineService do ...@@ -1099,6 +1099,62 @@ describe Ci::CreatePipelineService do
end end
end end
end end
context 'when needs is used' do
let(:pipeline) { execute_service }
let(:config) do
{
build_a: {
stage: "build",
script: "ls",
only: %w[master]
},
test_a: {
stage: "test",
script: "ls",
only: %w[master feature tags],
needs: %w[build_a]
},
deploy: {
stage: "deploy",
script: "ls",
only: %w[tags]
}
}
end
before do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
context 'when pipeline on master is created' do
let(:ref_name) { 'refs/heads/master' }
it 'creates a pipeline with build_a and test_a' do
expect(pipeline).to be_persisted
expect(pipeline.builds.map(&:name)).to contain_exactly("build_a", "test_a")
end
end
context 'when pipeline on feature is created' do
let(:ref_name) { 'refs/heads/feature' }
it 'does not create a pipeline as test_a depends on build_a' do
expect(pipeline).not_to be_persisted
expect(pipeline.builds).to be_empty
end
end
context 'when pipeline on v1.0.0 is created' do
let(:ref_name) { 'refs/tags/v1.0.0' }
it 'does create a pipeline only with deploy' do
expect(pipeline).to be_persisted
expect(pipeline.builds.map(&:name)).to contain_exactly("deploy")
end
end
end
end end
describe '#execute!' do describe '#execute!' do
......
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