Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 15:10:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 15:10:33 +0300
commitfdf32113c3924f7faec91101282fc28ec42fc869 (patch)
tree388fdb9982d5ae80c8bc9b9bdcc0dde98cd6ead9
parent5add82515889cf332b65bbf59394079222dc66b3 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml1
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue23
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/repository/constants.js2
-rw-r--r--app/assets/javascripts/search/sidebar/components/checkbox_filter.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter/index.vue2
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/new_card.scss15
-rw-r--r--app/models/ci/build.rb141
-rw-r--r--app/models/concerns/ci/deployable.rb159
-rw-r--r--app/views/admin/applications/index.html.haml87
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/environments/stop_job_success_worker.rb23
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/administration/dedicated/index.md2
-rw-r--r--doc/development/documentation/styleguide/word_list.md8
-rw-r--r--doc/topics/release_your_application.md1
-rw-r--r--doc/user/clusters/agent/gitops/example_repository_structure.md233
-rw-r--r--doc/user/clusters/agent/gitops/flux_oci_tutorial.md152
-rw-r--r--doc/user/group/insights/index.md4
-rw-r--r--lefthook.yml10
-rw-r--r--lib/sidebars/menu.rb4
-rw-r--r--locale/gitlab.pot44
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb14
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js2
-rw-r--r--spec/frontend/repository/components/blob_viewers/image_viewer_spec.js30
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js4
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js7
-rw-r--r--spec/lib/sidebars/menu_spec.rb13
-rw-r--r--spec/models/ci/build_spec.rb569
-rw-r--r--spec/support/shared_examples/ci/deployable_shared_examples.rb576
-rw-r--r--spec/workers/environments/stop_job_success_worker_spec.rb (renamed from spec/workers/build_success_worker_spec.rb)4
-rw-r--r--vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb79
-rwxr-xr-xvendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test9
-rwxr-xr-xvendor/languages.yml1
46 files changed, 1393 insertions, 896 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index d3d63955b9e..3a55d0f6747 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -4,7 +4,6 @@
when:
- api_failure
- data_integrity_failure
- - job_execution_timeout
- runner_system_failure
- scheduler_failure
- stuck_or_timeout_failure
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 6c3bb88f439..1e0795f98af 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-14.23.0
+14.24.1
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index d1e5e4eea13..aceae188b73 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -15,6 +15,7 @@ export const DATETIME_RANGE_TYPES = {
export const BV_SHOW_MODAL = 'bv::show::modal';
export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
+export const BV_SHOW_TOOLTIP = 'bv::show::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index a34ed065323..bdb78bab909 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -21,7 +21,13 @@ import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
-import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
+import {
+ DEFAULT_BLOB_INFO,
+ TEXT_FILE_TYPE,
+ LFS_STORAGE,
+ LEGACY_FILE_TYPES,
+ CODEOWNERS_FILE_NAME,
+} from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
@@ -32,6 +38,7 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
GlButton,
ForkSuggestion,
WebIdeLink,
@@ -79,7 +86,7 @@ export default {
const queryVariables = {
projectPath: this.projectPath,
filePath: this.path,
- ref: this.originalBranch || this.ref,
+ ref: this.currentRef,
refType: this.refType?.toUpperCase() || null,
shouldFetchRawText: true,
};
@@ -171,6 +178,12 @@ export default {
return nodes[0] || {};
},
+ currentRef() {
+ return this.originalBranch || this.ref;
+ },
+ isCodeownersFile() {
+ return this.path.includes(CODEOWNERS_FILE_NAME);
+ },
viewer() {
const { richViewer, simpleViewer } = this.blobInfo;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
@@ -402,6 +415,12 @@ export default {
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="path"
+ />
<blob-content
v-if="!blobViewer"
class="js-syntax-highlight"
diff --git a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
index 014f1abc121..9a8bb8e4aa6 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
@@ -8,7 +8,7 @@ export default {
},
data() {
return {
- url: this.blob.rawPath,
+ url: this.blob.externalStorageUrl || this.blob.rawPath,
alt: this.blob.name,
};
},
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index b711f671850..4327b237c8c 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -114,3 +114,5 @@ export const POLLING_INTERVAL_BACKOFF = 2;
export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal';
export const FORK_UPDATED_EVENT = 'fork:updated';
+
+export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
diff --git a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
index feff3f77dd2..bca049e56c7 100644
--- a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
@@ -27,7 +27,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['queryLanguageFilters']),
dataFilters() {
return Object.values(this.filtersData?.filters || []);
@@ -69,7 +69,9 @@ export default {
<template>
<div class="gl-mx-5">
- <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useNewNavigation }">{{ filtersData.header }}</h5>
+ <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ {{ filtersData.header }}
+ </h5>
<gl-form-checkbox-group v-model="selectedFilter">
<gl-form-checkbox
v-for="f in dataFilters"
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
index 7909aa9234b..312092b9904 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
@@ -10,7 +10,7 @@ export default {
RadioFilter,
},
computed: {
- ...mapState(['useNewNavigation']),
+ ...mapState(['useSidebarNavigation']),
},
confidentialFilterData,
HR_DEFAULT_CLASSES,
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 3eb025327a2..5fd8b6418d7 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -28,7 +28,7 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
showReset() {
return this.urlQuery.state || this.urlQuery.confidential || this.urlQuery.labels;
@@ -69,12 +69,12 @@ export default {
<template>
<form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
<hr
- v-if="!useNewNavigation && showConfidentialityFilter && showLabelFilter"
+ v-if="!useSidebarNavigation && showConfidentialityFilter && showLabelFilter"
:class="hrClasses"
/>
<label-filter v-if="showLabelFilter" />
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
index b820ca837bc..b42fe9185cb 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
@@ -27,7 +27,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['queryLanguageFilters']),
dataFilters() {
return Object.values(this.filtersData?.filters || []);
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index c10b14bd116..e5560dd5b55 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -36,7 +36,7 @@ export default {
reset: s__('GlobalSearch|Reset filters'),
},
computed: {
- ...mapState(['aggregations', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['aggregations', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters([
'languageAggregationBuckets',
'currentUrlQueryHasLanguageFilters',
@@ -120,8 +120,8 @@ export default {
class="gl-m-5 gl-my-0 language-filter-checkbox"
@submit.prevent="submitQuery"
>
- <hr v-if="!useNewNavigation" :class="dividerClassesTop" />
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }">
+ <hr v-if="!useSidebarNavigation" :class="dividerClassesTop" />
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ $options.languageFilterData.header }}
</h5>
<div
@@ -153,7 +153,7 @@ export default {
</gl-button>
</div>
<div v-if="!aggregations.error">
- <hr v-if="!useNewNavigation" :class="dividerClassesBottom" />
+ <hr v-if="!useSidebarNavigation" :class="dividerClassesBottom" />
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-mt-4">
<gl-button
category="primary"
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index 10ece1b82eb..8ad403ab31b 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -16,7 +16,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
ANY() {
return this.filterData.filters.ANY;
@@ -56,7 +56,7 @@ export default {
<template>
<div>
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }">
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ filterData.header }}
</h5>
<gl-form-radio-group v-model="selectedFilter">
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
index a9addb87f7b..88e434cf99e 100644
--- a/app/assets/javascripts/search/sidebar/components/results_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -16,7 +16,7 @@ export default {
ConfidentialityFilter,
},
computed: {
- ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
showReset() {
return this.urlQuery.state || this.urlQuery.confidential;
@@ -39,7 +39,7 @@ export default {
<template>
<form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery">
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" />
<confidentiality-filter v-if="showConfidentialityFilter" />
<div class="gl-display-flex gl-align-items-center gl-mt-4 gl-px-5">
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
index 494d75db6ce..010cfbad590 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
@@ -10,7 +10,7 @@ export default {
RadioFilter,
},
computed: {
- ...mapState(['useNewNavigation']),
+ ...mapState(['useSidebarNavigation']),
},
statusFilterData,
HR_DEFAULT_CLASSES,
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 2e88b45d646..613e504c771 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -263,6 +263,11 @@ span.idiff {
}
}
+.file-validation {
+ // we use $gray-light variable instead of utility class, because it's value is dynamic per color theme
+ background-color: $gray-light;
+}
+
.blob-content-holder .file-actions {
@include media-breakpoint-down(sm) {
.btn {
diff --git a/app/assets/stylesheets/framework/new_card.scss b/app/assets/stylesheets/framework/new_card.scss
index 48a834858c6..411f5300120 100644
--- a/app/assets/stylesheets/framework/new_card.scss
+++ b/app/assets/stylesheets/framework/new_card.scss
@@ -95,6 +95,10 @@
// Table adjustments
@mixin new-card-table-adjustments {
tbody > tr {
+ &:first-of-type > td[data-label] {
+ @include gl-border-t-0;
+ }
+
> td[data-label] {
@include gl-border-left-0;
@include gl-border-l-none;
@@ -119,8 +123,15 @@
table.b-table-stacked-sm,
table.b-table-stacked-md {
- @include gl-mt-n1;
- @include gl-mb-n2;
+ @include gl-mb-0;
+
+ tr:first-of-type th {
+ @include gl-border-t-0;
+ }
+
+ tr:last-of-type td {
+ @include gl-border-b-0;
+ }
}
table.gl-table.b-table.b-table-stacked-sm {
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index da70361743b..1ce852d4f71 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -5,6 +5,7 @@ module Ci
prepend Ci::BulkInsertableTags
include Ci::Metadatable
include Ci::Contextable
+ include Ci::Deployable
include TokenAuthenticatable
include AfterCommitQueue
include Presentable
@@ -34,7 +35,6 @@ module Ci
DEPLOYMENT_NAMES = %w[deploy release rollout].freeze
- has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable
has_one :pending_state, class_name: 'Ci::BuildPendingState', foreign_key: :build_id, inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id, inverse_of: :build
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id, inverse_of: :build
@@ -327,7 +327,6 @@ module Ci
after_transition any => [:success] do |build|
build.run_after_commit do
- BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
@@ -345,18 +344,6 @@ module Ci
end
end
end
-
- # Synchronize Deployment Status
- # Please note that the data integirty is not assured because we can't use
- # a database transaction due to DB decomposition.
- after_transition do |build, transition|
- next if transition.loopback?
- next unless build.project
-
- build.run_after_commit do
- build.deployment&.sync_status_with(build)
- end
- end
end
def self.build_matchers(project)
@@ -428,15 +415,6 @@ module Ci
action? && !archived? && (manual? || scheduled? || retryable?)
end
- def outdated_deployment?
- strong_memoize(:outdated_deployment) do
- deployment_job? &&
- project.ci_forward_deployment_enabled? &&
- (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
- deployment&.older_than_last_successful_deployment?
- end
- end
-
def schedulable?
self.when == 'delayed' && options[:start_in].present?
end
@@ -478,94 +456,10 @@ module Ci
Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet
end
- def persisted_environment
- return unless has_environment_keyword?
-
- strong_memoize(:persisted_environment) do
- # This code path has caused N+1s in the past, since environments are only indirectly
- # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
- # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
- BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
- Environment.where(name: names, project: args[:key]).find_each do |environment|
- loader.call(environment.name, environment)
- end
- end
- end
- end
-
- def persisted_environment=(environment)
- strong_memoize(:persisted_environment) { environment }
- end
-
- # If build.persisted_environment is a BatchLoader, we need to remove
- # the method proxy in order to clone into new item here
- # https://github.com/exAspArk/batch-loader/issues/31
- def actual_persisted_environment
- persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment
- end
-
- def expanded_environment_name
- return unless has_environment_keyword?
-
- strong_memoize(:expanded_environment_name) do
- # We're using a persisted expanded environment name in order to avoid
- # variable expansion per request.
- if metadata&.expanded_environment_name.present?
- metadata.expanded_environment_name
- else
- ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all })
- end
- end
- end
-
- def expanded_kubernetes_namespace
- return unless has_environment_keyword?
-
- namespace = options.dig(:environment, :kubernetes, :namespace)
-
- if namespace.present?
- strong_memoize(:expanded_kubernetes_namespace) do
- ExpandVariables.expand(namespace, -> { simple_variables })
- end
- end
- end
-
- def has_environment_keyword?
- environment.present?
- end
-
- def deployment_job?
- has_environment_keyword? && environment_action == 'start'
- end
-
- def stops_environment?
- has_environment_keyword? && environment_action == 'stop'
- end
-
- def environment_action
- options.fetch(:environment, {}).fetch(:action, 'start') if options
- end
-
- def environment_tier_from_options
- options.dig(:environment, :deployment_tier) if options
- end
-
- def environment_tier
- environment_tier_from_options || persisted_environment.try(:tier)
- end
-
def triggered_by?(current_user)
user == current_user
end
- def on_stop
- options&.dig(:environment, :on_stop)
- end
-
- def stop_action_successful?
- success?
- end
-
##
# All variables, including persisted environment variables.
#
@@ -1033,19 +927,6 @@ module Ci
job_artifacts.all_reports
end
- # Virtual deployment status depending on the environment status.
- def deployment_status
- return unless deployment_job?
-
- if success?
- return successful_deployment_status
- elsif failed?
- return :failed
- end
-
- :creating
- end
-
# Consider this object to have a structural integrity problems
def doom!
transaction do
@@ -1206,31 +1087,11 @@ module Ci
strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end
- def successful_deployment_status
- if deployment&.last?
- :last
- else
- :out_of_date
- end
- end
-
def job_artifacts_for_types(report_types)
# Use select to leverage cached associations and avoid N+1 queries
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
end
- def environment_url
- options&.dig(:environment, :url) || persisted_environment&.external_url
- end
-
- def environment_status
- strong_memoize(:environment_status) do
- if has_environment_keyword? && merge_request
- EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
- end
- end
- end
-
def has_expiring_artifacts?
artifacts_expire_at.present? && artifacts_expire_at > Time.current
end
diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb
new file mode 100644
index 00000000000..ffdb38f76ac
--- /dev/null
+++ b/app/models/concerns/ci/deployable.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+# rubocop:disable Gitlab/StrongMemoizeAttr
+module Ci
+ module Deployable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable
+
+ state_machine :status do
+ after_transition any => [:success] do |job|
+ job.run_after_commit do
+ Environments::StopJobSuccessWorker.perform_async(id)
+ end
+ end
+
+ # Synchronize Deployment Status
+ # Please note that the data integirty is not assured because we can't use
+ # a database transaction due to DB decomposition.
+ after_transition do |job, transition|
+ next if transition.loopback?
+ next unless job.project
+
+ job.run_after_commit do
+ job.deployment&.sync_status_with(job)
+ end
+ end
+ end
+ end
+
+ def outdated_deployment?
+ strong_memoize(:outdated_deployment) do
+ deployment_job? &&
+ project.ci_forward_deployment_enabled? &&
+ (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
+ deployment&.older_than_last_successful_deployment?
+ end
+ end
+
+ # Virtual deployment status depending on the environment status.
+ def deployment_status
+ return unless deployment_job?
+
+ if success?
+ return successful_deployment_status
+ elsif failed?
+ return :failed
+ end
+
+ :creating
+ end
+
+ def successful_deployment_status
+ if deployment&.last?
+ :last
+ else
+ :out_of_date
+ end
+ end
+
+ def persisted_environment
+ return unless has_environment_keyword?
+
+ strong_memoize(:persisted_environment) do
+ # This code path has caused N+1s in the past, since environments are only indirectly
+ # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
+ # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
+ BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
+ Environment.where(name: names, project: args[:key]).find_each do |environment|
+ loader.call(environment.name, environment)
+ end
+ end
+ end
+ end
+
+ def persisted_environment=(environment)
+ strong_memoize(:persisted_environment) { environment }
+ end
+
+ # If build.persisted_environment is a BatchLoader, we need to remove
+ # the method proxy in order to clone into new item here
+ # https://github.com/exAspArk/batch-loader/issues/31
+ def actual_persisted_environment
+ persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment
+ end
+
+ def expanded_environment_name
+ return unless has_environment_keyword?
+
+ strong_memoize(:expanded_environment_name) do
+ # We're using a persisted expanded environment name in order to avoid
+ # variable expansion per request.
+ if metadata&.expanded_environment_name.present?
+ metadata.expanded_environment_name
+ else
+ ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all })
+ end
+ end
+ end
+
+ def expanded_kubernetes_namespace
+ return unless has_environment_keyword?
+
+ namespace = options.dig(:environment, :kubernetes, :namespace)
+
+ if namespace.present? # rubocop:disable Style/GuardClause
+ strong_memoize(:expanded_kubernetes_namespace) do
+ ExpandVariables.expand(namespace, -> { simple_variables })
+ end
+ end
+ end
+
+ def has_environment_keyword?
+ environment.present?
+ end
+
+ def deployment_job?
+ has_environment_keyword? && environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment_keyword? && environment_action == 'stop'
+ end
+
+ def environment_action
+ options.fetch(:environment, {}).fetch(:action, 'start') if options
+ end
+
+ def environment_tier_from_options
+ options.dig(:environment, :deployment_tier) if options
+ end
+
+ def environment_tier
+ environment_tier_from_options || persisted_environment.try(:tier)
+ end
+
+ def environment_url
+ options&.dig(:environment, :url) || persisted_environment&.external_url
+ end
+
+ def environment_status
+ strong_memoize(:environment_status) do
+ if has_environment_keyword? && merge_request
+ EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
+ end
+ end
+ end
+
+ def on_stop
+ options&.dig(:environment, :on_stop)
+ end
+
+ def stop_action_successful?
+ success?
+ end
+ end
+end
+# rubocop:enable Gitlab/StrongMemoizeAttr
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index e32a50e252d..063045033b6 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -1,52 +1,51 @@
- page_title s_('AdminArea|Instance OAuth applications')
-%h1.page-title.gl-font-size-h-display
- = s_('AdminArea|Instance OAuth applications')
-%p.light
- - docs_link_path = help_page_path('integration/oauth_provider')
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path }
- = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper.gl-flex-direction-column
+ %h3.gl-new-card-title
+ = s_('AdminArea|Instance OAuth applications')
+ .gl-new-card-count
+ = sprite_icon('applications', css_class: 'gl-mr-2')
+ = @applications.size
+ %p.gl-new-card-description
+ - docs_link_path = help_page_path('integration/oauth_provider')
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path }
+ = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
-- if @applications.empty?
- %section.empty-state.gl-text-center.gl-display-flex.gl-flex-direction-column
- .svg-content.svg-150
- = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full'
-
- .gl-max-w-full.gl-m-auto
- %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found')
- = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, href: new_admin_application_path, button_options: { data: { qa_selector: 'new_application_button' } }) do
= _('New application')
+ - c.with_body do
+ - if @applications.empty?
+ %section.empty-state.gl-my-5.gl-text-center.gl-display-flex.gl-flex-direction-column
+ .svg-content.svg-150
+ = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full'
-- else
- %hr
- = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do
- = _('New application')
-
- .table-responsive
- %table.b-table.gl-table.gl-w-full{ role: 'table' }
- %thead
- %tr
- %th
- = _('Name')
- %th
- = _('Callback URL')
- %th
- = _('Trusted')
- %th
- = _('Confidential')
- %th
- %th
- %tbody.oauth-applications
- - @applications.each do |application|
- %tr{ id: "application_#{application.id}" }
- %td= link_to application.name, admin_application_path(application)
- %td= application.redirect_uri
- %td= application.trusted? ? _('Yes'): _('No')
- %td= application.confidential? ? _('Yes'): _('No')
- %td
- = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), variant: :link) do
- = _('Edit')
- %td= render 'delete_form', application: application
+ .gl-max-w-full.gl-m-auto
+ %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found')
+ %p.gl-text-secondary.gl-mt-3= s_('AdminArea|Manage applications for your instance that can use GitLab as an OAuth provider, start by creating a new one above.')
+ - else
+ .table-holder
+ %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead
+ %tr
+ %th= _('Name')
+ %th= _('Callback URL')
+ %th= _('Trusted')
+ %th= _('Confidential')
+ %th= _('Actions')
+ %tbody.oauth-applications
+ - @applications.each do |application|
+ %tr{ id: "application_#{application.id}" }
+ %td{ data: { label: _('Name') } }= link_to application.name, admin_application_path(application)
+ %td{ data: { label: _('Callback URL') } }= application.redirect_uri
+ %td{ data: { label: _('Trusted') } }= application.trusted? ? _('Yes'): _('No')
+ %td{ data: { label: _('Confidential') } }= application.confidential? ? _('Yes'): _('No')
+ %td{ data: { label: _('Actions') } }
+ = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), size: :small, button_options: { class: 'gl-mr-3' }) do
+ = _('Edit')
+ = render 'delete_form', application: application
= paginate @applications, theme: 'gitlab'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e41c85d1550..89bb1d11d1d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2712,6 +2712,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: environments_stop_job_success
+ :worker_name: Environments::StopJobSuccessWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: error_tracking_issue_link
:worker_name: ErrorTrackingIssueLinkWorker
:feature_category: :error_tracking
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 247105d2a1a..f5baa220715 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# Deprecated and will be removed in 17.0.
+# Use `Environments::StopJobSuccessWorker` instead.
class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
diff --git a/app/workers/environments/stop_job_success_worker.rb b/app/workers/environments/stop_job_success_worker.rb
new file mode 100644
index 00000000000..cc7d83512f3
--- /dev/null
+++ b/app/workers/environments/stop_job_success_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Environments
+ class StopJobSuccessWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+ idempotent!
+ feature_category :continuous_delivery
+
+ def perform(job_id, _params = {})
+ Ci::Build.find_by_id(job_id).try do |build|
+ stop_environment(build) if build.stops_environment? && build.stop_action_successful?
+ end
+ end
+
+ private
+
+ def stop_environment(build)
+ build.persisted_environment.fire_state_event(:stop_complete)
+ end
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 4f6889bf445..31b058e9e85 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -227,6 +227,8 @@
- 1
- - environments_canary_ingress_update
- 1
+- - environments_stop_job_success
+ - 1
- - epics
- 2
- - epics_new_epic_issue
diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md
index 4102d114066..870a342ef9a 100644
--- a/doc/administration/dedicated/index.md
+++ b/doc/administration/dedicated/index.md
@@ -63,6 +63,8 @@ The keys provided have to reside in the same primary, secondary and backup regio
For instructions on how to create and manage KMS keys, visit [Managing keys](https://docs.aws.amazon.com/kms/latest/developerguide/getting-started.html) in the AWS KMS documentation.
+GitLab Dedicated supports only AWS managed KMS keys with KMS [as key material](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-origin).
+
To create a KMS key using the AWS Console:
1. In `Configure key`, select:
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 85b42379c5c..a1c3b1220eb 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -259,6 +259,10 @@ Use **cannot** instead of **can not**.
See also [contractions](index.md#contractions).
+## Chat, GitLab Duo Chat
+
+Use **Chat** with a capital `c` for **Chat** or **GitLab Duo Chat**.
+
## checkbox
Use one word for **checkbox**. Do not use **check box**.
@@ -621,6 +625,10 @@ Use title case for **Geo**.
Do not make **GitLab** possessive (GitLab's). This guidance follows [GitLab Trademark Guidelines](https://about.gitlab.com/handbook/marketing/brand-and-product-marketing/brand/brand-activation/trademark-guidelines/).
+## GitLab Duo
+
+Do not use **Duo** by itself. Always use **GitLab Duo**.
+
## GitLab Flavored Markdown
When possible, spell out [**GitLab Flavored Markdown**](../../../user/markdown.md).
diff --git a/doc/topics/release_your_application.md b/doc/topics/release_your_application.md
index 3cc5e9a66b3..c3a504c59f2 100644
--- a/doc/topics/release_your_application.md
+++ b/doc/topics/release_your_application.md
@@ -28,7 +28,6 @@ release features incrementally.
- [Auto Deploy](autodevops/stages.md#auto-deploy) is the DevOps stage dedicated to software
deployment using GitLab CI/CD. Auto Deploy has built-in support for EC2 and ECS deployments.
- Deploy to Kubernetes clusters by using the [GitLab agent](../user/clusters/agent/install/index.md).
-- View an example of [how to structure a GitOps deployment repository](../user/clusters/agent/gitops/example_repository_structure.md).
- Use Docker images to run AWS commands from GitLab CI/CD, and a template to
facilitate [deployment to AWS](../ci/cloud_deployment).
- Use GitLab CI/CD to target any type of infrastructure accessible by GitLab Runner.
diff --git a/doc/user/clusters/agent/gitops/example_repository_structure.md b/doc/user/clusters/agent/gitops/example_repository_structure.md
index a5bc3b153fe..5c5970c9a54 100644
--- a/doc/user/clusters/agent/gitops/example_repository_structure.md
+++ b/doc/user/clusters/agent/gitops/example_repository_structure.md
@@ -1,78 +1,177 @@
---
stage: Deploy
group: Environments
-info: An example of how to structure a repository for GitOps deployments
+info: A tutorial for structuring a repository for GitOps deployments
---
-# Example GitOps repository structure **(FREE)**
+# Tutorial: Structure your repository for GitOps deployments **(FREE)**
-This page describes an example structure for a project that builds and deploys an application
-to a Kubernetes cluster with [GitOps](https://about.gitlab.com/topics/gitops) and the
-[GitLab agent for Kubernetes](../../agent/gitops.md).
+In this tutorial, you'll create a GitLab project that builds and deploys an application
+to a Kubernetes cluster using Flux. You'll set up a sample manifest project, configure it to
+push manifests to a deployment branch, and configure Flux to sync the deployment branch.
+
+This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).
+
+To set up a repository for GitOps deployments:
+
+1. [Create the Kubernetes manifest repository](#create-the-kubernetes-manifest-repository)
+1. [Create a deployment branch](#create-a-deployment-branch)
+1. [Configure GitLab CI/CD to push to your branch](#configure-gitlab-cicd-to-push-to-your-branch)
+1. [Configure Flux to sync your manifests](#configure-flux-to-sync-your-manifests)
+1. [Verify your configuration](#verify-your-configuration)
+
+Prerequisites:
-You can find an example project that uses this structure
-[in this GitLab repository](https://gitlab.com/tigerwnz/minimal-gitops-app). You can use the example project
-as a starting point to create your own deployment project.
+- You have a Flux repository connected to a Kubernetes cluster.
+ If you're starting from scratch, see [Set up Flux for GitOps](flux_tutorial.md).
-## Deployment workflow
+## Create the Kubernetes manifest repository
-The default branch is the single source of truth for your application and the
-Kubernetes manifests that deploy it. To be reflected in a Kubernetes cluster,
-a code or configuration change must exist in the default branch.
+First, create a repository for your Kubernetes manifests:
-A GitLab agent for Kubernetes is installed in every Kubernetes cluster. The agent
-is configured to sync manifests from a corresponding branch in the repository.
-These branches represent the state of each cluster, and contain only commits that
-exist in the default branch.
+1. In GitLab, create a new repository called `web-app-manifests`.
+1. In `web-app-manifests`, add a file named `src/nginx-deployment.yaml` with the following contents:
-Changes are deployed by merging the default branch into the branch of a cluster.
-The agent that watches the branch picks up the change and syncs it to the cluster.
-
-For the actual deployment, the example project uses the GitLab agent for Kubernetes,
-but you can also use other GitOps tools.
-
-### Review apps
-
-Ephemeral environments such as [review apps](../../../../ci/review_apps/index.md)
-are deployed differently. Their configuration does not exist on the default branch,
-and the changes are not meant to be deployed to a permanent environment. Review app
-manifests are generated and deployed in a merge request feature branch, which is removed
-when the MR is merged.
-
-## Example deployment
-
-The example project deploys to two permanent environments, staging and production,
-which each have a dedicated Kubernetes cluster. A third cluster is used for ephemeral
-review apps.
-
-Each cluster has a corresponding branch that represents the current state of the cluster:
-`_gitlab/agents/staging`, `_gitlab/agents/production` and `_gitlab/agents/review`. Each branch is
-[protected](../../../../user/project/protected_branches.md) and
-a [project access token](../../../../user/project/settings/project_access_tokens.md)
-is created for each branch with a configuration that allows only the corresponding token to push to the branch.
-This ensures that environment branches are updated only through the configured process.
-
-Deployment branches are updated by CI/CD jobs. The access token that allows pushing to each
-branch is configured as a [CI/CD variable](../../../../ci/variables/index.md). These variables
-are protected, and only available to pipelines running on a protected branch.
-The CI/CD job merges the default branch `main` into the deployment branch, and pushes
-the deployment branch back to the repository using the provided token. To preserve the
-commit history between both branches, the CI/CD job uses a fast-forward merge.
-
-Each cluster has an agent for Kubernetes, and each agent is configured to
-sync manifests from the branch corresponding to its cluster.
-In your own project, you can different GitOps tool like Flux, or use the same configuration to deploy
-to virtual machines with GitLab CI/CD.
-
-### Application changes
-
-The example project follows this process to deploy an application change:
-
-1. A new feature branch is created with the desired changes. The pipeline builds an image,
- runs the test suite, and deploy the changes to a review app in the `review` cluster.
-1. The feature branch is merged to `main` and the review app is removed.
-1. Manifests are updated on `main` (either directly or via merge request) to point to an updated
- version of the deployed image. The pipeline automatically merges `main` into the `_gitlab/agents/staging`
- branch, which updates the `staging` cluster.
-1. The `production` job is triggered manually, and merges `main` into the `_gitlab/agents/production` branch,
- deploying to the `production` cluster.
+ ```yaml
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: nginx
+ spec:
+ replicas: 1
+ template:
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ ports:
+ - containerPort: 80
+ ```
+
+1. In `web-app-manifests`, add a file named `src/kustomization.yaml` with the following contents:
+
+ ```yaml
+ apiVersion: kustomize.config.k8s.io/v1beta1
+ kind: Kustomization
+ resources:
+ - nginx-deployment.yaml
+ commonLabels:
+ app: flux-branches-tutorial
+ ```
+
+## Create a deployment branch
+
+Next, create a branch to reflect the current state of your cluster.
+
+In this workflow, the default branch is the single source of truth for your application.
+To be reflected in a Kubernetes cluster, a code or configuration change must exist in the default branch.
+In a later step, you'll configure CI/CD to merge changes from the default branch into the deployment branch.
+
+To create a deployment branch:
+
+1. In `web-app-manifests`, create a branch named `_gitlab/deploy/example` from the default branch. The branch name in this example is chosen to
+ differentiate the deployment branch from feature branches, but this is not required. You can name the deployment branch whatever you like.
+1. Create a [project](../../../../user/project/settings/project_access_tokens.md),
+ [group](../../../../user/group/settings/group_access_tokens.md) or
+ [personal access token](../../../../user/profile/personal_access_tokens.md) with the `write_repository` scope.
+1. Create a [CI/CD variable](../../../../ci/variables/index.md) with a token value named `DEPLOYMENT_TOKEN`.
+ Remember to [mask](../../../../ci/variables/index.md#mask-a-cicd-variable) the value so that it won't show in
+ job logs.
+1. Add a rule to [protect](../../../../user/project/protected_branches.md)
+ your deployment branch with the following values:
+
+ - Allowed to merge: No one.
+ - Allowed to push and merge: Select the token you created in the previous step, or your user if you created
+ a personal access token.
+ - Allowed to force push: Turn off the toggle.
+ - Require approval from code owners: Turn off the toggle.
+
+This configuration ensures that only the corresponding token can push to the branch.
+
+You've successfully created a repository with a protected deployment branch!
+
+## Configure GitLab CI/CD to push to your branch
+
+Next, you'll configure CI/CD to merge changes from the default branch to your deployment branch.
+
+In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents:
+
+ ```yaml
+ deploy:
+ stage: deploy
+ environment: production
+ variables:
+ DEPLOYMENT_BRANCH: _gitlab/deploy/example
+ script:
+ - |
+ git config user.name "Deploy Example Bot"
+ git config user.email "test@example.com"
+ git fetch origin $DEPLOYMENT_BRANCH
+ git checkout $DEPLOYMENT_BRANCH
+ git merge $CI_COMMIT_SHA --ff-only
+ git push https://deploy:$DEPLOYMENT_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git HEAD:$DEPLOYMENT_BRANCH
+ resource_group: $CI_ENVIRONMENT_SLUG
+ ```
+
+This creates a CI/CD pipeline with a single `deploy` job that:
+
+1. Checks out your deployment branch.
+1. Merges new changes from the default branch into the deployment branch.
+1. Pushes the changes to your repository with the configured token.
+
+## Configure Flux to sync your manifests
+
+Next, configure your Flux repository to sync the deployment branch in by the `web-app-manifests` repository.
+
+To configure, create a [`GitRepository`](https://fluxcd.io/flux/components/source/gitrepositories/) resource:
+
+1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-source.yaml`
+ with the following contents:
+
+ ```yaml
+ apiVersion: source.toolkit.fluxcd.io/v1
+ kind: GitRepository
+ metadata:
+ name: web-app-manifests
+ namespace: flux-system
+ spec:
+ interval: 5m0s
+ url: https://gitlab.com/gitlab-org/configure/examples/flux/web-app-manifests-branches
+ ref:
+ branch: _gitlab/deploy/example
+ ```
+
+ You will need to substitute the `url` with the URL of your `web-app-manifests` project.
+
+1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-kustomization.yaml`
+ with the following contents:
+
+ ```yaml
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
+ kind: Kustomization
+ metadata:
+ name: nginx-source-kustomization
+ namespace: flux-system
+ spec:
+ interval: 1m0s
+ path: ./src
+ prune: true
+ sourceRef:
+ kind: GitRepository
+ name: web-app-manifests
+ targetNamespace: default
+ ```
+
+ This file adds a [Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/) resource that tells Flux to sync the manifests in the artifact fetched from the registry.
+
+1. Commit the new files and push.
+
+## Verify your configuration
+
+After the pipeline completes, you should see a newly created `nginx` pod in your cluster.
+
+If you want to see the deployment sync again, try updating the number of replicas in the
+`src/nginx-deployment.yaml` file and push to the default branch. If all is working well, the change
+will sync to the cluster when the pipeline has finished.
+
+Congratulations! You successfully configured a project to deploy an application and synchronize your changes!
diff --git a/doc/user/clusters/agent/gitops/flux_oci_tutorial.md b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md
new file mode 100644
index 00000000000..71f3293fcd8
--- /dev/null
+++ b/doc/user/clusters/agent/gitops/flux_oci_tutorial.md
@@ -0,0 +1,152 @@
+---
+stage: Deploy
+group: Environments
+info: A tutorial for deploying an OCI artifact using Flux
+---
+
+# Tutorial: Deploy an OCI artifact using Flux **(FREE)**
+
+This tutorial teaches you how to package your Kubernetes manifests into an [OCI](https://opencontainers.org/)
+artifact and deploy them to your cluster using Flux. You'll set up a sample manifest project, configure it to
+store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact.
+
+This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).
+
+To deploy an OCI artifact using Flux:
+
+1. [Create the Kubernetes manifest repository](#create-the-kubernetes-manifest-repository)
+1. [Configure the manifest repository to create an OCI artifact](#configure-the-manifest-repository-to-create-an-oci-artifact)
+1. [Configure Flux to sync your artifact](#configure-flux-to-sync-your-artifact)
+1. [Verify your configuration](#verify-your-configuration)
+
+Prerequisites:
+
+- You have a Flux repository connected to a Kubernetes cluster.
+ If you're starting from scratch, see [Set up Flux for GitOps](flux_tutorial.md).
+
+## Create the Kubernetes manifest repository
+
+First, create a repository for your Kubernetes manifests:
+
+1. In GitLab, create a new repository called `web-app-manifests`.
+1. In `web-app-manifests`, add a file named `src/nginx-deployment.yaml` with the following contents:
+
+ ```yaml
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: nginx
+ spec:
+ replicas: 1
+ template:
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ ports:
+ - containerPort: 80
+ ```
+
+1. In `web-app-manifests`, add a file named `src/kustomization.yaml` with the following contents:
+
+ ```yaml
+ apiVersion: kustomize.config.k8s.io/v1beta1
+ kind: Kustomization
+ resources:
+ - nginx-deployment.yaml
+ commonLabels:
+ app: flux-oci-tutorial
+ ```
+
+## Configure the manifest repository to create an OCI artifact
+
+Next, configure [GitLab CI/CD](../../../../ci/index.md) to package your manifests into an OCI artifact,
+and push the artifact to the [GitLab Container Registry](../../../packages/container_registry/index.md):
+
+1. In the root of `web-app-manifests`, create and push a [`.gitlab-ci.yml`](../../../../ci/yaml/gitlab_ci_yaml.md) file with the following contents:
+
+ ```yaml
+ package:
+ stage: deploy
+ image:
+ name: fluxcd/flux-cli:v2.0.0-rc.1
+ entrypoint: [""]
+ script:
+ - mkdir -p manifests
+ - kubectl kustomize ./src --output ./manifests
+ - |
+ flux push artifact oci://$CI_REGISTRY_IMAGE:latest \
+ --path="./manifests" \
+ --source="$CI_REPOSITORY_URL" \
+ --revision="$CI_COMMIT_SHORT_SHA" \
+ --creds="$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" \
+ --annotations="org.opencontainers.image.url=$CI_PROJECT_URL" \
+ --annotations="org.opencontainers.image.title=$CI_PROJECT_NAME" \
+ --annotations="com.gitlab.job.id=$CI_JOB_ID" \
+ --annotations="com.gitlab.job.url=$CI_JOB_URL"
+ ```
+
+ When the file is pushed to GitLab, a CI/CD pipeline with a single `package` job is created. This job:
+
+ - Uses `kustomization.yaml` to render your final Kubernetes manifests.
+ - Packages your manifests into an OCI artifact.
+ - Pushes the OCI artifact to the Container Registry.
+
+ After the pipeline has completed, you can check your OCI artifact with the Container Registry UI.
+
+## Configure Flux to sync your artifact
+
+Next, configure your Flux repository to sync the artifact produced by the `web-app-manifests` repository.
+
+To configure, create an [`OCIRepository`](https://fluxcd.io/flux/components/source/ocirepositories/) resource:
+
+1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-source.yaml`
+ with the following contents:
+
+ ```yaml
+ apiVersion: source.toolkit.fluxcd.io/v1
+ kind: OCIRepository
+ metadata:
+ name: web-app-manifests
+ namespace: flux-system
+ spec:
+ interval: 1m0s
+ url: oci://registry.gitlab.com/gitlab-org/configure/examples/flux/web-app-manifests-oci
+ ref:
+ tag: latest
+ ```
+
+ You will need to substitute the `url` with the URL of your `web-app-manifests` project's container registry.
+
+1. In your local clone of your Flux repository, add a file named `clusters/my-cluster/web-app-manifests-kustomization.yaml`
+ with the following contents:
+
+ ```yaml
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
+ kind: Kustomization
+ metadata:
+ name: nginx-source-kustomization
+ namespace: flux-system
+ spec:
+ interval: 1m0s
+ path: ./
+ prune: true
+ sourceRef:
+ kind: OCIRepository
+ name: web-app-manifests
+ targetNamespace: default
+ ```
+
+ This file adds a [Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/) resource that tells Flux to sync the manifests in the artifact fetched from the registry.
+
+1. Commit the new files and push.
+
+## Verify your configuration
+
+You should see a newly created `nginx` pod in your cluster.
+
+If you want to see the deployment sync again, try updating the number of replicas in the
+`src/nginx-deployment.yaml` file and push to the default branch. If all is working well, the change
+should sync to the cluster when the pipeline has finished.
+
+Congratulations! You successfully configured a project to deploy an application and synchronize your changes!
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
index ab967c8b12c..61ec055c6ee 100644
--- a/doc/user/group/insights/index.md
+++ b/doc/user/group/insights/index.md
@@ -47,8 +47,8 @@ Insights display data from the last 90 days. You can zoom in to display data onl
To do this, select the pause icons (**{status-paused}**) and slide them along the horizontal axis:
-- To select a later start date, slide the left pause icon to the right.
-- To select an earlier end date, slide the right pause icon to the left.
+- To change the start date, slide the left pause icon to the left or right.
+- To change the end date, slide the right pause icon to the left or right.
### Exclude dimensions from charts
diff --git a/lefthook.yml b/lefthook.yml
index bed3593ba3d..53f1b6201ad 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -41,11 +41,6 @@ pre-push:
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{js,vue,graphql}'
run: yarn run prettier --check {files}
- rubocop:
- tags: backend style
- files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
- glob: '*.{rb,rake}'
- run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
sidekiq-queues:
tags: backend
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
@@ -115,6 +110,11 @@ pre-push:
pre-commit:
parallel: true
commands:
+ rubocop:
+ tags: backend style
+ files: git diff --name-only --diff-filter=d --staged
+ glob: '*.{rb,rake}'
+ run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
secrets-detection:
tags: secrets
files: git diff --name-only --diff-filter=d --staged
diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb
index 5f9255c06d0..73d6f733da5 100644
--- a/lib/sidebars/menu.rb
+++ b/lib/sidebars/menu.rb
@@ -123,6 +123,10 @@ module Sidebars
insert_element_after(@items, after_item, new_item)
end
+ def remove_item(item)
+ remove_element(@items, item.item_id)
+ end
+
def replace_placeholder(item)
idx = @items.index { |e| e.item_id == item.item_id && e.is_a?(::Sidebars::NilMenuItem) }
if idx.nil?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ddf2e681513..772d563c6dc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3163,6 +3163,9 @@ msgstr ""
msgid "AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}."
msgstr ""
+msgid "AdminArea|Manage applications for your instance that can use GitLab as an OAuth provider, start by creating a new one above."
+msgstr ""
+
msgid "AdminArea|Minimal access"
msgstr ""
@@ -11353,6 +11356,47 @@ msgstr ""
msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions currently uses third-party AI services unless those are %{third_party_features_link_start}disabled%{link_end}."
msgstr ""
+msgid "CodeownersValidation|An error occurred while loading the validation errors. Please try again later."
+msgstr ""
+
+msgid "CodeownersValidation|Contains %d syntax error."
+msgid_plural "CodeownersValidation|Contains %d syntax errors."
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "CodeownersValidation|Entries with spaces"
+msgstr ""
+
+msgid "CodeownersValidation|Hide errors"
+msgstr ""
+
+msgid "CodeownersValidation|How are errors handled?"
+msgstr ""
+
+msgid "CodeownersValidation|Inaccessible owners"
+msgstr ""
+
+msgid "CodeownersValidation|Less than 1 required approvals"
+msgstr ""
+
+msgid "CodeownersValidation|Line"
+msgstr ""
+
+msgid "CodeownersValidation|Missing section name"
+msgstr ""
+
+msgid "CodeownersValidation|Show errors"
+msgstr ""
+
+msgid "CodeownersValidation|Syntax is valid."
+msgstr ""
+
+msgid "CodeownersValidation|Unparsable sections"
+msgstr ""
+
+msgid "CodeownersValidation|Zero owners"
+msgstr ""
+
msgid "Cohorts|Inactive users"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb
index c2db5062962..3c656d9ca75 100644
--- a/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/saas/pull_container_registry_image_spec.rb
@@ -2,16 +2,18 @@
module QA
RSpec.describe 'Package' do
- describe 'SaaS Container Registry', :smoke, only: { subdomain: %i[staging] }, product_group: :container_registry do
- let(:project) do
- Resource::Project.init do |project|
- project.path_with_namespace = 'gitlab-qa/container-registry-sanity'
- end.reload!
+ describe 'SaaS Container Registry', :smoke,
+ only: { subdomain: :staging }, product_group: :container_registry do
+ before do
+ Flow::Login.sign_in
end
it 'pulls an image from an existing repository',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412799' do
- Flow::Login.sign_in
+ project = Resource::Project.init do |project|
+ project.path_with_namespace = 'gitlab-qa/container-registry-sanity'
+ end.reload!
+
project.visit!
Page::Project::Menu.perform(&:go_to_pipelines)
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 0217835b2a3..53a43626691 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -35,7 +35,7 @@ Vue.config.productionTip = false;
Vue.use(Translate);
-const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
+const JQUERY_MATCHERS_TO_EXCLUDE = ['toBeEmpty', 'toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
diff --git a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
index c23de0efdfd..4455851529d 100644
--- a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
@@ -7,19 +7,35 @@ describe('Image Viewer', () => {
const DEFAULT_BLOB_DATA = {
rawPath: 'some/image.png',
name: 'image.png',
+ externalStorageUrl: '',
};
- const createComponent = () => {
- wrapper = shallowMount(ImageViewer, { propsData: { blob: DEFAULT_BLOB_DATA } });
+ const createComponent = (blobData = DEFAULT_BLOB_DATA) => {
+ wrapper = shallowMount(ImageViewer, { propsData: { blob: blobData } });
};
const findImage = () => wrapper.find('[data-testid="image"]');
- it('renders a Source Editor component', () => {
- createComponent();
+ describe('When blob has externalStorageUrl', () => {
+ const externalStorageUrl = 'http://img.server.com/lfs-object/21/45/foo_bar';
- expect(findImage().exists()).toBe(true);
- expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
- expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
+ it('renders a Source Editor component with externalStorageUrl', () => {
+ const blobData = { ...DEFAULT_BLOB_DATA, externalStorageUrl };
+ createComponent(blobData);
+
+ expect(findImage().exists()).toBe(true);
+ expect(findImage().attributes('src')).toBe(externalStorageUrl);
+ expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
+ });
+ });
+
+ describe('When blob does not have an externalStorageUrl', () => {
+ it('renders a Source Editor component with rawPath', () => {
+ createComponent(DEFAULT_BLOB_DATA);
+
+ expect(findImage().exists()).toBe(true);
+ expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index ef7f3359bad..68054a341a2 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -23,7 +23,7 @@ describe('ConfidentialityFilter', () => {
describe('old sidebar', () => {
beforeEach(() => {
- createComponent({ useNewNavigation: false });
+ createComponent({ useSidebarNavigation: false });
});
it('renders the component', () => {
@@ -33,7 +33,7 @@ describe('ConfidentialityFilter', () => {
describe('new sidebar', () => {
beforeEach(() => {
- createComponent({ useNewNavigation: true });
+ createComponent({ useSidebarNavigation: true });
});
it('renders the component', () => {
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 2cf5ae2a70a..fd705d5976b 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -23,7 +23,7 @@ describe('StatusFilter', () => {
describe('old sidebar', () => {
beforeEach(() => {
- createComponent({ useNewNavigation: false });
+ createComponent({ useSidebarNavigation: false });
});
it('renders the component', () => {
@@ -33,7 +33,7 @@ describe('StatusFilter', () => {
describe('new sidebar', () => {
beforeEach(() => {
- createComponent({ useNewNavigation: true });
+ createComponent({ useSidebarNavigation: true });
});
it('renders the component', () => {
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 08a9c2a42d8..271c99be57a 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,7 +1,8 @@
import { GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, createWrapper as makeWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { BV_HIDE_TOOLTIP, BV_SHOW_TOOLTIP } from '~/lib/utils/constants';
import initCopyToClipboard, {
CLIPBOARD_SUCCESS_EVENT,
CLIPBOARD_ERROR_EVENT,
@@ -31,7 +32,7 @@ describe('clipboard button', () => {
title,
});
- wrapper.vm.$root.$emit = jest.fn();
+ const rootWrapper = makeWrapper(wrapper.vm.$root);
const button = findButton();
@@ -42,7 +43,7 @@ describe('clipboard button', () => {
await button.trigger(event);
- expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1');
+ expect(rootWrapper.emitted(BV_SHOW_TOOLTIP)[0]).toContain('clipboard-button-1');
expect(button.attributes()).toMatchObject({
title: message,
@@ -56,7 +57,7 @@ describe('clipboard button', () => {
title,
'aria-label': title,
});
- expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
+ expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)[0]).toContain('clipboard-button-1');
};
describe('without gfm', () => {
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index 2f8f97c5b95..7f3cf9820db 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -27,16 +27,19 @@ describe('modal copy button', () => {
wrapper.trigger('click');
await nextTick();
- expect(wrapper.emitted().success).not.toBeEmpty();
+ expect(wrapper.emitted('error')).toBeUndefined();
+ expect(wrapper.emitted('success')).toHaveLength(1);
expect(document.execCommand).toHaveBeenCalledWith('copy');
expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
});
+
it("should propagate the clipboard error event if execCommand doesn't work", async () => {
document.execCommand = jest.fn(() => false);
wrapper.trigger('click');
await nextTick();
- expect(wrapper.emitted().error).not.toBeEmpty();
+ expect(wrapper.emitted('success')).toBeUndefined();
+ expect(wrapper.emitted('error')).toHaveLength(1);
expect(document.execCommand).toHaveBeenCalledWith('copy');
});
});
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index 4f77cb3aed4..00202ac7d2b 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -302,6 +302,19 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do
end
end
+ describe "#remove_item" do
+ let(:item) { Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}, item_id: :foo1) }
+
+ before do
+ menu.add_item(item)
+ end
+
+ it 'removes the item from the menu' do
+ menu.remove_item(item)
+ expect(menu.has_items?).to be false
+ end
+ end
+
describe '#container_html_options' do
before do
allow(menu).to receive(:title).and_return('Foo Menu')
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index d3ef92eafec..bfd6360527f 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -38,7 +38,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) }
it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) }
- it { is_expected.to have_one(:deployment) }
it { is_expected.to have_one(:runner_manager).through(:runner_manager_build) }
it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) }
it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) }
@@ -67,14 +66,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
- shared_examples 'calling proper BuildFinishedWorker' do
- it 'calls Ci::BuildFinishedWorker' do
- expect(Ci::BuildFinishedWorker).to receive(:perform_async)
-
- subject
- end
- end
-
describe 'associations' do
it 'has a bidirectional relationship with projects' do
expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds)
@@ -109,6 +100,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it_behaves_like 'has ID tokens', :ci_build
it_behaves_like 'a retryable job'
+ it_behaves_like 'a deployable job'
describe '.manual_actions' do
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
@@ -657,64 +649,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#outdated_deployment?' do
- subject { build.outdated_deployment? }
-
- let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') }
-
- context 'when build has no environment' do
- let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) }
-
- it { expect(subject).to be_falsey }
- end
-
- context 'when project has forward deployment disabled' do
- before do
- project.ci_cd_settings.update!(forward_deployment_enabled: false)
- end
-
- it { expect(subject).to be_falsey }
- end
-
- context 'when build is not an outdated deployment' do
- before do
- allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
- end
-
- it { expect(subject).to be_falsey }
- end
-
- context 'when build is older than the latest deployment and still pending status' do
- before do
- allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
- end
-
- it { expect(subject).to be_truthy }
- end
-
- context 'when build is older than the latest deployment but succeeded once' do
- let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
-
- before do
- allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
- end
-
- it 'returns false for allowing rollback' do
- expect(subject).to be_falsey
- end
-
- context 'when forward_deployment_rollback_allowed option is disabled' do
- before do
- project.ci_cd_settings.update!(forward_deployment_rollback_allowed: false)
- end
-
- it 'returns true for disallowing rollback' do
- expect(subject).to eq(true)
- end
- end
- end
- end
-
describe '#schedulable?' do
subject { build.schedulable? }
@@ -1598,430 +1532,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe 'state transition as a deployable' do
- subject { build.send(event) }
-
- let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) }
- let(:deployment) { build.deployment }
- let(:environment) { deployment.environment }
-
- before do
- allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
- allow(Deployments::HooksWorker).to receive(:perform_async)
- end
-
- it 'has deployments record with created status' do
- expect(deployment).to be_created
- expect(environment.name).to eq('review/master')
- end
-
- shared_examples_for 'avoid deadlock' do
- it 'executes UPDATE in the right order' do
- recorded = with_cross_database_modification_prevented do
- ActiveRecord::QueryRecorder.new { subject }
- end
-
- index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") }
- index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") }
-
- expect(index_for_build).to be < index_for_deployment
- end
- end
-
- context 'when transits to running' do
- let(:event) { :run! }
-
- it_behaves_like 'avoid deadlock'
-
- it 'transits deployment status to running' do
- with_cross_database_modification_prevented do
- subject
- end
-
- expect(deployment).to be_running
- end
-
- context 'when deployment is already running state' do
- before do
- build.deployment.success!
- end
-
- it 'does not change deployment status and tracks an error' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception).with(
- instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
-
- with_cross_database_modification_prevented do
- expect { subject }.not_to change { deployment.reload.status }
- end
- end
- end
- end
-
- context 'when transits to success' do
- let(:event) { :success! }
-
- before do
- allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
- allow(Deployments::HooksWorker).to receive(:perform_async)
- end
-
- it_behaves_like 'avoid deadlock'
- it_behaves_like 'calling proper BuildFinishedWorker'
-
- it 'transits deployment status to success' do
- with_cross_database_modification_prevented do
- subject
- end
-
- expect(deployment).to be_success
- end
- end
-
- context 'when transits to failed' do
- let(:event) { :drop! }
-
- it_behaves_like 'avoid deadlock'
- it_behaves_like 'calling proper BuildFinishedWorker'
-
- it 'transits deployment status to failed' do
- with_cross_database_modification_prevented do
- subject
- end
-
- expect(deployment).to be_failed
- end
- end
-
- context 'when transits to skipped' do
- let(:event) { :skip! }
-
- it_behaves_like 'avoid deadlock'
-
- it 'transits deployment status to skipped' do
- with_cross_database_modification_prevented do
- subject
- end
-
- expect(deployment).to be_skipped
- end
- end
-
- context 'when transits to canceled' do
- let(:event) { :cancel! }
-
- it_behaves_like 'avoid deadlock'
- it_behaves_like 'calling proper BuildFinishedWorker'
-
- it 'transits deployment status to canceled' do
- with_cross_database_modification_prevented do
- subject
- end
-
- expect(deployment).to be_canceled
- end
- end
-
- # Mimic playing a manual job that needs another job.
- # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502
- context 'when transits from skipped to created to running' do
- before do
- build.skip!
- end
-
- context 'during skipped to created' do
- let(:event) { :process! }
-
- it 'transitions to created' do
- subject
-
- expect(deployment).to be_created
- end
- end
-
- context 'during created to running' do
- let(:event) { :run! }
-
- before do
- build.process!
- build.enqueue!
- end
-
- it 'transitions to running and calls webhook' do
- freeze_time do
- expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s }))
-
- subject
- end
-
- expect(deployment).to be_running
- end
- end
- end
- end
-
- describe '#on_stop' do
- subject { build.on_stop }
-
- context 'when a job has a specification that it can be stopped from the other job' do
- let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) }
-
- it 'returns the other job name' do
- is_expected.to eq('stop_review_app')
- end
- end
-
- context 'when a job does not have environment information' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#environment_tier_from_options' do
- subject { build.environment_tier_from_options }
-
- let(:build) { described_class.new(options: options) }
- let(:options) { { environment: { deployment_tier: 'production' } } }
-
- it { is_expected.to eq('production') }
-
- context 'when options does not include deployment_tier' do
- let(:options) { { environment: { name: 'production' } } }
-
- it { is_expected.to be_nil }
- end
- end
-
- describe '#environment_tier' do
- subject { build.environment_tier }
-
- let(:options) { { environment: { deployment_tier: 'production' } } }
- let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) }
- let(:build) { described_class.new(options: options, environment: 'production', project: project) }
-
- it { is_expected.to eq('production') }
-
- context 'when options does not include deployment_tier' do
- let(:options) { { environment: { name: 'production' } } }
-
- it 'uses tier from environment' do
- is_expected.to eq('development')
- end
-
- context 'when persisted environment is absent' do
- let(:environment) { nil }
-
- it { is_expected.to be_nil }
- end
- end
- end
-
- describe 'environment' do
- describe '#has_environment_keyword?' do
- subject { build.has_environment_keyword? }
-
- context 'when environment is defined' do
- before do
- build.update!(environment: 'review')
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when environment is not defined' do
- before do
- build.update!(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#expanded_environment_name' do
- subject { build.expanded_environment_name }
-
- context 'when environment uses $CI_COMMIT_REF_NAME' do
- let(:build) do
- create(
- :ci_build,
- ref: 'master',
- environment: 'review/$CI_COMMIT_REF_NAME',
- pipeline: pipeline
- )
- end
-
- it { is_expected.to eq('review/master') }
- end
-
- context 'when environment uses yaml_variables containing symbol keys' do
- let(:build) do
- create(
- :ci_build,
- yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST',
- pipeline: pipeline
- )
- end
-
- it 'returns an expanded environment name with a list of variables' do
- is_expected.to eq('review/host')
- end
-
- context 'when build metadata has already persisted the expanded environment name' do
- before do
- build.metadata.expanded_environment_name = 'review/foo'
- end
-
- it 'returns a persisted expanded environment name without a list of variables' do
- expect(build).not_to receive(:simple_variables)
-
- is_expected.to eq('review/foo')
- end
- end
- end
-
- context 'when using persisted variables' do
- let(:build) do
- create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
- end
-
- it { is_expected.to eq('review/x') }
- end
-
- context 'when environment name uses a nested variable' do
- let(:yaml_variables) do
- [
- { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' }
- ]
- end
-
- let(:build) do
- create(
- :ci_build,
- ref: 'master',
- yaml_variables: yaml_variables,
- environment: 'review/$ENVIRONMENT_NAME',
- pipeline: pipeline
- )
- end
-
- it { is_expected.to eq('review/master') }
- end
- end
-
- describe '#expanded_kubernetes_namespace' do
- let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) }
-
- subject { build.expanded_kubernetes_namespace }
-
- context 'environment and namespace are not set' do
- let(:environment) { nil }
- let(:options) { nil }
-
- it { is_expected.to be_nil }
- end
-
- context 'environment is specified' do
- let(:environment) { 'production' }
-
- context 'namespace is not set' do
- let(:options) { nil }
-
- it { is_expected.to be_nil }
- end
-
- context 'namespace is provided' do
- let(:options) do
- {
- environment: {
- name: environment,
- kubernetes: {
- namespace: namespace
- }
- }
- }
- end
-
- context 'with a static value' do
- let(:namespace) { 'production' }
-
- it { is_expected.to eq namespace }
- end
-
- context 'with a dynamic value' do
- let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' }
-
- it { is_expected.to eq 'deploy-master' }
- end
- end
- end
- end
-
- describe '#deployment_job?' do
- subject { build.deployment_job? }
-
- context 'when environment is defined' do
- before do
- build.update!(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_truthy }
- end
-
- context 'and start action is defined' do
- before do
- build.update!(options: { environment: { action: 'start' } })
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update!(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#stops_environment?' do
- subject { build.stops_environment? }
-
- context 'when environment is defined' do
- before do
- build.update!(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_falsey }
- end
-
- context 'and stop action is defined' do
- before do
- build.update!(options: { environment: { action: 'stop' } })
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update!(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
describe 'erasable build' do
shared_examples 'erasable' do
it 'removes artifact file' do
@@ -2560,44 +2070,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#persisted_environment' do
- let!(:environment) do
- create(:environment, project: project, name: "foo-#{project.default_branch}")
- end
-
- subject { build.persisted_environment }
-
- context 'when referenced literally' do
- let(:build) do
- create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}")
- end
-
- it { is_expected.to eq(environment) }
- end
-
- context 'when referenced with a variable' do
- let(:build) do
- create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
- end
-
- it { is_expected.to eq(environment) }
- end
-
- context 'when there is no environment' do
- it { is_expected.to be_nil }
- end
-
- context 'when build has a stop environment' do
- let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
-
- it 'expands environment name' do
- expect(build).to receive(:expanded_environment_name).and_call_original
-
- is_expected.to eq(environment)
- end
- end
- end
-
describe '#play' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
@@ -5061,45 +4533,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#deployment_status' do
- before do
- allow_any_instance_of(described_class).to receive(:create_deployment)
- end
-
- context 'when build is a last deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
- let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
-
- it { expect(build.deployment_status).to eq(:last) }
- end
-
- context 'when there is a newer build with deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
- let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
- let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
-
- it { expect(build.deployment_status).to eq(:out_of_date) }
- end
-
- context 'when build with deployment has failed' do
- let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) }
- let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
-
- it { expect(build.deployment_status).to eq(:failed) }
- end
-
- context 'when build with deployment is running' do
- let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) }
- let(:environment) { create(:environment, name: 'production', project: build.project) }
- let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
-
- it { expect(build.deployment_status).to eq(:creating) }
- end
- end
-
describe '#degenerated?' do
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated, pipeline: pipeline) }
diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb
new file mode 100644
index 00000000000..682e408566b
--- /dev/null
+++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb
@@ -0,0 +1,576 @@
+# frozen_string_literal: true
+
+# rubocop:disable Layout/LineLength
+# rubocop:disable RSpec/ContextWording
+RSpec.shared_examples 'a deployable job' do
+ it { is_expected.to have_one(:deployment) }
+
+ shared_examples 'calling proper BuildFinishedWorker' do
+ it 'calls Ci::BuildFinishedWorker' do
+ expect(Ci::BuildFinishedWorker).to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
+
+ let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') }
+
+ context 'when build has no environment' do
+ let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when project has forward deployment disabled' do
+ before do
+ project.ci_cd_settings.update!(forward_deployment_enabled: false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when build is not an outdated deployment' do
+ before do
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when build is older than the latest deployment and still pending status' do
+ before do
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'when build is older than the latest deployment but succeeded once' do
+ let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
+
+ before do
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it 'returns false for allowing rollback' do
+ expect(subject).to be_falsey
+ end
+
+ context 'when forward_deployment_rollback_allowed option is disabled' do
+ before do
+ project.ci_cd_settings.update!(forward_deployment_rollback_allowed: false)
+ end
+
+ it 'returns true for disallowing rollback' do
+ expect(subject).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe 'state transition as a deployable' do
+ subject { build.send(event) }
+
+ let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) }
+ let(:deployment) { build.deployment }
+ let(:environment) { deployment.environment }
+
+ before do
+ allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
+ end
+
+ it 'has deployments record with created status' do
+ expect(deployment).to be_created
+ expect(environment.name).to eq('review/master')
+ end
+
+ shared_examples_for 'avoid deadlock' do
+ it 'executes UPDATE in the right order' do
+ recorded = with_cross_database_modification_prevented do
+ ActiveRecord::QueryRecorder.new { subject }
+ end
+
+ index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") }
+ index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") }
+
+ expect(index_for_build).to be < index_for_deployment
+ end
+ end
+
+ context 'when transits to running' do
+ let(:event) { :run! }
+
+ it_behaves_like 'avoid deadlock'
+
+ it 'transits deployment status to running' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_running
+ end
+
+ context 'when deployment is already running state' do
+ before do
+ build.deployment.success!
+ end
+
+ it 'does not change deployment status and tracks an error' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(
+ instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
+
+ with_cross_database_modification_prevented do
+ expect { subject }.not_to change { deployment.reload.status }
+ end
+ end
+ end
+ end
+
+ context 'when transits to success' do
+ let(:event) { :success! }
+
+ before do
+ allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to success' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_success
+ end
+ end
+
+ context 'when transits to failed' do
+ let(:event) { :drop! }
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to failed' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_failed
+ end
+ end
+
+ context 'when transits to skipped' do
+ let(:event) { :skip! }
+
+ it_behaves_like 'avoid deadlock'
+
+ it 'transits deployment status to skipped' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_skipped
+ end
+ end
+
+ context 'when transits to canceled' do
+ let(:event) { :cancel! }
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to canceled' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_canceled
+ end
+ end
+
+ # Mimic playing a manual job that needs another job.
+ # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502
+ context 'when transits from skipped to created to running' do
+ before do
+ build.skip!
+ end
+
+ context 'during skipped to created' do
+ let(:event) { :process! }
+
+ it 'transitions to created' do
+ subject
+
+ expect(deployment).to be_created
+ end
+ end
+
+ context 'during created to running' do
+ let(:event) { :run! }
+
+ before do
+ build.process!
+ build.enqueue!
+ end
+
+ it 'transitions to running and calls webhook' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s }))
+
+ subject
+ end
+
+ expect(deployment).to be_running
+ end
+ end
+ end
+ end
+
+ describe '#on_stop' do
+ subject { build.on_stop }
+
+ context 'when a job has a specification that it can be stopped from the other job' do
+ let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) }
+
+ it 'returns the other job name' do
+ is_expected.to eq('stop_review_app')
+ end
+ end
+
+ context 'when a job does not have environment information' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#environment_tier_from_options' do
+ subject { build.environment_tier_from_options }
+
+ let(:build) { Ci::Build.new(options: options) }
+ let(:options) { { environment: { deployment_tier: 'production' } } }
+
+ it { is_expected.to eq('production') }
+
+ context 'when options does not include deployment_tier' do
+ let(:options) { { environment: { name: 'production' } } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#environment_tier' do
+ subject { build.environment_tier }
+
+ let(:options) { { environment: { deployment_tier: 'production' } } }
+ let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) }
+ let(:build) { Ci::Build.new(options: options, environment: 'production', project: project) }
+
+ it { is_expected.to eq('production') }
+
+ context 'when options does not include deployment_tier' do
+ let(:options) { { environment: { name: 'production' } } }
+
+ it 'uses tier from environment' do
+ is_expected.to eq('development')
+ end
+
+ context 'when persisted environment is absent' do
+ let(:environment) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe 'environment' do
+ describe '#has_environment_keyword?' do
+ subject { build.has_environment_keyword? }
+
+ context 'when environment is defined' do
+ before do
+ build.update!(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { build.expanded_environment_name }
+
+ context 'when environment uses $CI_COMMIT_REF_NAME' do
+ let(:build) do
+ create(
+ :ci_build,
+ ref: 'master',
+ environment: 'review/$CI_COMMIT_REF_NAME',
+ pipeline: pipeline
+ )
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+
+ context 'when environment uses yaml_variables containing symbol keys' do
+ let(:build) do
+ create(
+ :ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST',
+ pipeline: pipeline
+ )
+ end
+
+ it 'returns an expanded environment name with a list of variables' do
+ is_expected.to eq('review/host')
+ end
+
+ context 'when build metadata has already persisted the expanded environment name' do
+ before do
+ build.metadata.expanded_environment_name = 'review/foo'
+ end
+
+ it 'returns a persisted expanded environment name without a list of variables' do
+ expect(build).not_to receive(:simple_variables)
+
+ is_expected.to eq('review/foo')
+ end
+ end
+ end
+
+ context 'when using persisted variables' do
+ let(:build) do
+ create(:ci_build, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
+ end
+
+ it { is_expected.to eq('review/x') }
+ end
+
+ context 'when environment name uses a nested variable' do
+ let(:yaml_variables) do
+ [
+ { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' }
+ ]
+ end
+
+ let(:build) do
+ create(
+ :ci_build,
+ ref: 'master',
+ yaml_variables: yaml_variables,
+ environment: 'review/$ENVIRONMENT_NAME',
+ pipeline: pipeline
+ )
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+ end
+
+ describe '#expanded_kubernetes_namespace' do
+ let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) }
+
+ subject { build.expanded_kubernetes_namespace }
+
+ context 'environment and namespace are not set' do
+ let(:environment) { nil }
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'environment is specified' do
+ let(:environment) { 'production' }
+
+ context 'namespace is not set' do
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'namespace is provided' do
+ let(:options) do
+ {
+ environment: {
+ name: environment,
+ kubernetes: {
+ namespace: namespace
+ }
+ }
+ }
+ end
+
+ context 'with a static value' do
+ let(:namespace) { 'production' }
+
+ it { is_expected.to eq namespace }
+ end
+
+ context 'with a dynamic value' do
+ let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' }
+
+ it { is_expected.to eq 'deploy-master' }
+ end
+ end
+ end
+ end
+
+ describe '#deployment_job?' do
+ subject { build.deployment_job? }
+
+ context 'when environment is defined' do
+ before do
+ build.update!(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ build.update!(options: { environment: { action: 'start' } })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { build.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update!(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ build.update!(options: { environment: { action: 'stop' } })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#persisted_environment' do
+ let!(:environment) do
+ create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'when referenced literally' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}")
+ end
+
+ it { is_expected.to eq(environment) }
+ end
+
+ context 'when referenced with a variable' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
+ end
+
+ it { is_expected.to eq(environment) }
+ end
+
+ context 'when there is no environment' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when build has a stop environment' do
+ let(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it 'expands environment name' do
+ expect(build).to receive(:expanded_environment_name).and_call_original
+
+ is_expected.to eq(environment)
+ end
+ end
+ end
+
+ describe '#deployment_status' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf
+ end
+
+ context 'when build is a last deployment' do
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: build.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
+
+ it { expect(build.deployment_status).to eq(:last) }
+ end
+
+ context 'when there is a newer build with deployment' do
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: build.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
+ let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
+
+ it { expect(build.deployment_status).to eq(:out_of_date) }
+ end
+
+ context 'when build with deployment has failed' do
+ let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: build.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
+
+ it { expect(build.deployment_status).to eq(:failed) }
+ end
+
+ context 'when build with deployment is running' do
+ let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: build.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
+
+ it { expect(build.deployment_status).to eq(:creating) }
+ end
+ end
+end
+# rubocop:enable Layout/LineLength
+# rubocop:enable RSpec/ContextWording
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/environments/stop_job_success_worker_spec.rb
index be9802eb2ce..3a2db8cfb77 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/environments/stop_job_success_worker_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe BuildSuccessWorker, feature_category: :continuous_integration do
+RSpec.describe Environments::StopJobSuccessWorker, feature_category: :continuous_delivery do
describe '#perform' do
subject { described_class.new.perform(build.id) }
context 'when build exists' do
context 'when the build will stop an environment' do
- let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, status: :success) }
+ let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, status: :success) } # rubocop:disable Layout/LineLength
let(:environment) { create(:environment, state: :available) }
it 'stops the environment' do
diff --git a/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb b/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb
index 01da04ce27c..0f1249dcf71 100644
--- a/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb
+++ b/vendor/gems/bundler-checksum/lib/bundler_checksum/command/lint.rb
@@ -7,35 +7,86 @@ module BundlerChecksum::Command
extend self
def execute
- linted = true
+ definition = Bundler.definition
+ definition.validate_runtime!
+ definition.resolve_only_locally!
- Bundler.definition.resolve.sort_by(&:name).each do |spec|
+ errors = lint_specs(definition.specs.sort_by(&:name))
+ show_errors(errors)
+
+ !errors.any?
+ end
+
+ private
+
+ def lint_specs(specs)
+ specs.filter_map do |spec|
next unless spec.source.is_a?(Bundler::Source::Rubygems)
+ next if default_gem_without_cache_file?(spec)
+
+ lint_spec(spec)
+ end
+ end
+
+ def lint_spec(spec)
+ expected_checksum = expected_checksum_for(spec)
- unless checksum_for?(spec.name)
- $stderr.puts "ERROR: Missing checksum for gem `#{spec.name}`"
- linted = false
+ if expected_checksum
+ actual_checksum = actual_checksum_for(spec)
+
+ if expected_checksum != actual_checksum
+ <<~ERROR
+ #{error_message_for(spec, 'Invalid checksum')}
+
+ Expected: #{expected_checksum}
+ Actual: #{actual_checksum}
+ ERROR
end
+ else
+ error_message_for(spec, 'Missing checksum')
end
+ end
- unless linted
- $stderr.puts <<~MSG
+ def error_message_for(spec, message)
+ "ERROR: #{message} for gem `#{spec.name}` (#{spec.version} #{spec.platform})"
+ end
+
+ def show_errors(errors)
+ return if errors.none?
+
+ errors.each { |error| $stderr.puts error }
+
+ $stderr.puts <<~MSG
+
+ Please run `bundle exec bundler-checksum init` to add correct checksums.
+ MSG
+ end
- Please run `bundle exec bundler-checksum init` to add missing checksums.
- MSG
+ def default_gem_without_cache_file?(spec)
+ spec.default_gem? && !File.exist?(spec.cache_file)
+ end
+
+ def expected_checksum_for(spec)
+ info_list = gems_with_checksums.fetch(spec.name, [])
+
+ info = info_list.find do |hash|
+ hash[:version] == spec.version.to_s &&
+ hash[:platform] == spec.platform.to_s
end
- linted
+ info&.fetch(:checksum)
end
- private
+ def actual_checksum_for(spec)
+ path = spec.cache_file
- def checksum_for?(name)
- gems_with_checksums.include?(name)
+ Bundler::SharedHelpers.filesystem_access(path, :read) do
+ Bundler::SharedHelpers.digest(:SHA256).hexdigest(File.read(path))
+ end
end
def gems_with_checksums
- @gems_with_checksums ||= local_checksums.map { |hash| hash[:name] }.to_set
+ @gems_with_checksums ||= local_checksums.group_by { |hash| hash[:name] }
end
def local_checksums
diff --git a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test
index 9b6d83591f8..7006a98c626 100755
--- a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test
+++ b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/scripts/test
@@ -1,10 +1,6 @@
#!/bin/sh
-set -x
-set -e
-
-# Ensure that each gem has a checksum entry
-ruby -I ../../lib ../../bin/bundler-checksum lint
+set -xe
# Check there's no differences after re-initialising
ruby -I ../../lib ../../bin/bundler-checksum init
@@ -16,3 +12,6 @@ ruby -I ../../lib ../../bin/bundler-checksum verify
# Test installing with bundler-checksum
export BUNDLER_CHECKSUM_VERIFICATION_OPT_IN=1
bundle install
+
+# Ensure that each gem has a valid checksum entry
+ruby -I ../../lib ../../bin/bundler-checksum lint
diff --git a/vendor/languages.yml b/vendor/languages.yml
index 5e7a955b1bb..0a6f78ebe5d 100755
--- a/vendor/languages.yml
+++ b/vendor/languages.yml
@@ -6098,6 +6098,7 @@ XML:
- wsdl
extensions:
- ".xml"
+ - ".arxml"
- ".adml"
- ".admx"
- ".ant"