summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-07 15:09:49 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-07 15:09:49 +0000
commit84f9f0cb8137637708a41152347e7754c1e9fb83 (patch)
tree6db9d8931bdb3c5b932b36345373936e2a543126
parent75f809a2ff829574ab91628407993187d55e14a4 (diff)
downloadgitlab-ce-84f9f0cb8137637708a41152347e7754c1e9fb83.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/layout/array_alignment.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_popover.vue35
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue2
-rw-r--r--app/assets/javascripts/editor/schema/ci.json16
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue53
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/constants.js22
-rw-r--r--app/assets/stylesheets/page_bundles/settings.scss2
-rw-r--r--app/models/ci/job_artifact.rb7
-rw-r--r--app/models/environment.rb10
-rw-r--r--app/services/ci/components/fetch_service.rb54
-rw-r--r--config/events/1675167870_Gitlab__Ci__Pipeline__Chain__Metrics_create_pipeline_with_name.yml26
-rw-r--r--config/feature_flags/development/ci_include_components.yml8
-rw-r--r--config/feature_flags/development/validate_environment_tier_presence.yml7
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/settings.rb11
-rw-r--r--db/init_structure.sql2
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md4
-rw-r--r--doc/development/code_review.md1
-rw-r--r--doc/raketasks/restore_gitlab.md21
-rw-r--r--doc/user/project/issues/create_issues.md2
-rw-r--r--doc/user/shortcuts.md7
-rw-r--r--lib/gitlab/ci/build/auto_retry.rb4
-rw-r--r--lib/gitlab/ci/components/instance_path.rb75
-rw-r--r--lib/gitlab/ci/config/entry/include.rb6
-rw-r--r--lib/gitlab/ci/config/external/file/component.rb100
-rw-r--r--lib/gitlab/ci/config/external/mapper/matcher.rb7
-rw-r--r--lib/gitlab/ci/pipeline/chain/metrics.rb18
-rw-r--r--lib/gitlab/ci/reports/sbom/component.rb2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--spec/config/settings_spec.rb28
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js66
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js56
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb116
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb179
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb55
-rw-r--r--spec/models/ci/job_artifact_spec.rb8
-rw-r--r--spec/models/environment_spec.rb29
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb141
47 files changed, 1174 insertions, 113 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 94016d3d3a0..65750e500c1 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -2047,7 +2047,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/ci/ansi2json/line.rb'
- 'lib/gitlab/ci/config/entry/environment.rb'
- 'lib/gitlab/ci/config/entry/imageable.rb'
- - 'lib/gitlab/ci/config/entry/include.rb'
- 'lib/gitlab/ci/config/entry/job.rb'
- 'lib/gitlab/ci/config/entry/product/parallel.rb'
- 'lib/gitlab/ci/config/entry/pull_policy.rb'
diff --git a/.rubocop_todo/layout/array_alignment.yml b/.rubocop_todo/layout/array_alignment.yml
index 34a2eefcadf..de7139d1380 100644
--- a/.rubocop_todo/layout/array_alignment.yml
+++ b/.rubocop_todo/layout/array_alignment.yml
@@ -256,7 +256,6 @@ Layout/ArrayAlignment:
- 'spec/lib/gitlab/ci/config/external/file/remote_spec.rb'
- 'spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb'
- 'spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb'
- - 'spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb'
- 'spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb'
- 'spec/lib/gitlab/ci/config/external/mapper_spec.rb'
- 'spec/lib/gitlab/ci/config_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 198fada5fa1..f8f5c6d6dfa 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -1871,7 +1871,6 @@ Layout/LineLength:
- 'ee/spec/graphql/types/incident_management/escalation_rule_input_type_spec.rb'
- 'ee/spec/graphql/types/issue_type_spec.rb'
- 'ee/spec/graphql/types/permission_types/vulnerability_spec.rb'
- - 'ee/spec/graphql/types/pipeline_security_report_finding_type_spec.rb'
- 'ee/spec/graphql/types/project_type_spec.rb'
- 'ee/spec/graphql/types/security_scanner_type_enum_spec.rb'
- 'ee/spec/graphql/types/vulnerability_details/file_location_type_spec.rb'
diff --git a/app/assets/javascripts/analytics/shared/components/metric_popover.vue b/app/assets/javascripts/analytics/shared/components/metric_popover.vue
index 8d90e7b2392..373a7fac6f7 100644
--- a/app/assets/javascripts/analytics/shared/components/metric_popover.vue
+++ b/app/assets/javascripts/analytics/shared/components/metric_popover.vue
@@ -1,5 +1,6 @@
<script>
import { GlPopover, GlLink, GlIcon } from '@gitlab/ui';
+import { METRIC_POPOVER_LABEL } from '../constants';
export default {
name: 'MetricPopover',
@@ -19,34 +20,34 @@ export default {
},
},
computed: {
- metricLinks() {
- return this.metric.links?.filter((link) => !link.docs_link) || [];
+ metricLink() {
+ return this.metric.links?.find((link) => !link.docs_link);
},
docsLink() {
return this.metric.links?.find((link) => link.docs_link);
},
},
+ metricPopoverLabel: METRIC_POPOVER_LABEL,
};
</script>
<template>
- <gl-popover :target="target" placement="bottom">
+ <gl-popover :target="target" placement="top">
<template #title>
- <span class="gl-display-block gl-text-left" data-testid="metric-label">{{
- metric.label
- }}</span>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-text-right gl-py-1 gl-align-items-center"
+ >
+ <span data-testid="metric-label">{{ metric.label }}</span>
+ <gl-link
+ v-if="metricLink"
+ :href="metricLink.url"
+ class="gl-font-sm gl-font-weight-normal"
+ data-testid="metric-link"
+ >{{ $options.metricPopoverLabel }}
+ <gl-icon name="chart" />
+ </gl-link>
+ </div>
</template>
- <div
- v-for="(link, idx) in metricLinks"
- :key="`link-${idx}`"
- class="gl-display-flex gl-justify-content-space-between gl-text-right gl-py-1"
- data-testid="metric-link"
- >
- <span>{{ link.label }}</span>
- <gl-link :href="link.url" class="gl-font-sm">
- {{ link.name }}
- </gl-link>
- </div>
<span v-if="metric.description" data-testid="metric-description">{{ metric.description }}</span>
<gl-link
v-if="docsLink"
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index a82633035b5..7ced658f483 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -13,6 +13,8 @@ export const dateFormats = {
month: 'mmmm',
};
+export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
+
export const KEY_METRICS = {
LEAD_TIME: 'lead_time',
CYCLE_TIME: 'cycle_time',
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 84c29e48114..7368d1a3a91 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -52,7 +52,7 @@ export default {
};
</script>
<template>
- <div class="gl-mb-4">
+ <div class="gl-mb-4 gl-display-flex gl-flex-wrap gl-gap-3">
<gl-button
v-if="showFileTreeToggle"
id="file-tree-toggle"
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 324acb177b0..57477a993c5 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -417,6 +417,20 @@
"type": "object",
"additionalProperties": false,
"properties": {
+ "component": {
+ "description": "Local path to component directory or full path to external component directory.",
+ "type": "string",
+ "format": "uri-reference"
+ }
+ },
+ "required": [
+ "component"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
"remote": {
"description": "URL to a `yaml`/`yml` template file using HTTP/HTTPS.",
"type": "string",
@@ -1955,4 +1969,4 @@
"additionalProperties": false
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 9b669024a8b..f3d392a0ec4 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,23 +1,22 @@
<script>
-import { s__ } from '~/locale';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import { expandSection } from '~/settings_panels';
+import { scrollToElement } from '~/lib/utils/common_utils';
import BranchRule from './components/branch_rule.vue';
-
-export const i18n = {
- queryError: s__(
- 'ProtectedBranch|An error occurred while loading branch rules. Please try again.',
- ),
- emptyState: s__(
- 'ProtectedBranch|Protected branches, merge request approvals, and status checks will appear here once configured.',
- ),
-};
+import { I18N, PROTECTED_BRANCHES_ANCHOR, BRANCH_PROTECTION_MODAL_ID } from './constants';
export default {
name: 'BranchRules',
- i18n,
+ i18n: I18N,
components: {
BranchRule,
+ GlButton,
+ GlModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
},
apollo: {
branchRules: {
@@ -36,20 +35,27 @@ export default {
},
},
inject: {
- projectPath: {
- default: '',
- },
+ projectPath: { default: '' },
},
data() {
return {
branchRules: [],
};
},
+ methods: {
+ showProtectedBranches() {
+ // Protected branches section is on the same page as the branch rules section.
+ expandSection(this.$options.protectedBranchesAnchor);
+ scrollToElement(this.$options.protectedBranchesAnchor);
+ },
+ },
+ modalId: BRANCH_PROTECTION_MODAL_ID,
+ protectedBranchesAnchor: PROTECTED_BRANCHES_ANCHOR,
};
</script>
<template>
- <div class="settings-content">
+ <div class="settings-content gl-mb-0">
<branch-rule
v-for="(rule, index) in branchRules"
:key="`${rule.name}-${index}`"
@@ -61,6 +67,21 @@ export default {
:matching-branches-count="rule.matchingBranchesCount"
/>
- <span v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</span>
+ <div v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</div>
+
+ <gl-button v-gl-modal="$options.modalId" class="gl-mt-5" category="secondary" variant="info">{{
+ $options.i18n.addBranchRule
+ }}</gl-button>
+
+ <gl-modal
+ :ref="$options.modalId"
+ :modal-id="$options.modalId"
+ :title="$options.i18n.addBranchRule"
+ :ok-title="$options.i18n.createProtectedBranch"
+ @ok="showProtectedBranches"
+ >
+ <p>{{ $options.i18n.branchRuleModalDescription }}</p>
+ <p>{{ $options.i18n.branchRuleModalContent }}</p>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js
new file mode 100644
index 00000000000..4413d8eab4e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js
@@ -0,0 +1,22 @@
+import { s__ } from '~/locale';
+
+export const I18N = {
+ queryError: s__(
+ 'ProtectedBranch|An error occurred while loading branch rules. Please try again.',
+ ),
+ emptyState: s__(
+ 'ProtectedBranch|After you configure a protected branch, merge request approval, or status check, it appears here.',
+ ),
+ addBranchRule: s__('BranchRules|Add branch rule'),
+ branchRuleModalDescription: s__(
+ 'BranchRules|To create a branch rule, you first need to create a protected branch.',
+ ),
+ branchRuleModalContent: s__(
+ 'BranchRules|After a protected branch is created, it will show up in the list as a branch rule.',
+ ),
+ createProtectedBranch: s__('BranchRules|Create protected branch'),
+};
+
+export const PROTECTED_BRANCHES_ANCHOR = '#js-protected-branches-settings';
+
+export const BRANCH_PROTECTION_MODAL_ID = 'addBranchRuleModal';
diff --git a/app/assets/stylesheets/page_bundles/settings.scss b/app/assets/stylesheets/page_bundles/settings.scss
index 9037eb7ae62..8978b8d798b 100644
--- a/app/assets/stylesheets/page_bundles/settings.scss
+++ b/app/assets/stylesheets/page_bundles/settings.scss
@@ -71,6 +71,7 @@
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
pointer-events: none;
+ margin-bottom: $gl-spacing-scale-5;
.settings.expanded & {
max-height: none;
@@ -101,7 +102,6 @@
display: block;
height: 1px;
overflow: hidden;
- margin-top: 20px;
}
.sub-section {
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0dca5b18a24..b5622959c75 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -141,8 +141,11 @@ module Ci
before_save :set_size, if: :file_changed?
after_save :store_file_in_transaction!, unless: :store_after_commit?
+
after_commit :store_file_after_transaction!, on: [:create, :update], if: :store_after_commit?
+ after_destroy_commit :log_destroy
+
validates :job, presence: true
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
@@ -384,6 +387,10 @@ module Ci
# Use job.project to avoid extra DB query for project
job.project.pending_delete?
end
+
+ def log_destroy
+ Gitlab::Ci::Artifacts::Logger.log_deleted(self, __method__)
+ end
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 7d99f10822d..7febafc2cca 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -74,7 +74,11 @@ class Environment < ApplicationRecord
# Currently, the tier presence is validaed for newly created environments.
# After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/385253.
- validates :tier, presence: true, on: :create
+ # Todo: Remove along with FF `validate_environment_tier_presence`.
+ validates :tier, presence: true, on: :create, unless: :validate_environment_tier_present?
+
+ validates :tier, presence: true, if: :validate_environment_tier_present?
+
validate :safe_external_url
validate :merge_request_not_changed
@@ -600,6 +604,10 @@ class Environment < ApplicationRecord
self.class.tiers[:other]
end
end
+
+ def validate_environment_tier_present?
+ Feature.enabled?(:validate_environment_tier_presence, self.project)
+ end
end
Environment.prepend_mod_with('Environment')
diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb
new file mode 100644
index 00000000000..45abb415174
--- /dev/null
+++ b/app/services/ci/components/fetch_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Ci
+ module Components
+ class FetchService
+ include Gitlab::Utils::StrongMemoize
+
+ TEMPLATE_FILE = 'template.yml'
+
+ COMPONENT_PATHS = [
+ ::Gitlab::Ci::Components::InstancePath
+ ].freeze
+
+ def initialize(address:, current_user:)
+ @address = address
+ @current_user = current_user
+ end
+
+ def execute
+ unless component_path_class
+ return ServiceResponse.error(
+ message: "#{error_prefix} the component path is not supported",
+ reason: :unsupported_path)
+ end
+
+ component_path = component_path_class.new(address: address, content_filename: TEMPLATE_FILE)
+ content = component_path.fetch_content!(current_user: current_user)
+
+ if content.present?
+ ServiceResponse.success(payload: { content: content, path: component_path })
+ else
+ ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found)
+ end
+ rescue Gitlab::Access::AccessDeniedError
+ ServiceResponse.error(
+ message: "#{error_prefix} project does not exist or you don't have sufficient permissions",
+ reason: :not_allowed)
+ end
+
+ private
+
+ attr_reader :current_user, :address
+
+ def component_path_class
+ COMPONENT_PATHS.find { |klass| klass.match?(address) }
+ end
+ strong_memoize_attr :component_path_class
+
+ def error_prefix
+ "component '#{address}' -"
+ end
+ end
+ end
+end
diff --git a/config/events/1675167870_Gitlab__Ci__Pipeline__Chain__Metrics_create_pipeline_with_name.yml b/config/events/1675167870_Gitlab__Ci__Pipeline__Chain__Metrics_create_pipeline_with_name.yml
new file mode 100644
index 00000000000..0427f86fb27
--- /dev/null
+++ b/config/events/1675167870_Gitlab__Ci__Pipeline__Chain__Metrics_create_pipeline_with_name.yml
@@ -0,0 +1,26 @@
+---
+description: Pipeline with name is created
+category: Gitlab::Ci::Pipeline::Chain::Metrics
+action: create_pipeline_with_name
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+- project
+- user
+- namespace
+product_section: ops
+product_stage: verify
+product_group: pipeline_execution
+product_category: Continuous Integration
+milestone: "15.9"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109549
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
+
diff --git a/config/feature_flags/development/ci_include_components.yml b/config/feature_flags/development/ci_include_components.yml
new file mode 100644
index 00000000000..b61863e9675
--- /dev/null
+++ b/config/feature_flags/development/ci_include_components.yml
@@ -0,0 +1,8 @@
+---
+name: ci_include_components
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109154
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/39064
+milestone: '15.9'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/validate_environment_tier_presence.yml b/config/feature_flags/development/validate_environment_tier_presence.yml
new file mode 100644
index 00000000000..ca9f7be8e75
--- /dev/null
+++ b/config/feature_flags/development/validate_environment_tier_presence.yml
@@ -0,0 +1,7 @@
+name: validate_environment_tier_presence
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111011
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385253
+milestone: '15.9'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 06cb5e97a17..8a44e4077f4 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -265,6 +265,7 @@ Settings['gitlab_ci'] ||= Settingslogic.new({})
Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil?
Settings.gitlab_ci['builds_path'] = Settings.absolute(Settings.gitlab_ci['builds_path'] || "builds/")
Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci_url)
+Settings.gitlab_ci['component_fqdn'] ||= Settings.__send__(:build_ci_component_fqdn)
#
# CI Secure Files
diff --git a/config/settings.rb b/config/settings.rb
index 34acb09b9ed..ae95af802c4 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -11,6 +11,17 @@ class Settings < Settingslogic
on_standard_port?(gitlab)
end
+ def build_ci_component_fqdn
+ custom_port = ":#{gitlab.port}" unless on_standard_port?(gitlab)
+
+ [
+ gitlab.host,
+ custom_port,
+ gitlab.relative_url_root,
+ '/'
+ ].join('')
+ end
+
def host_without_www(url)
host(url).sub('www.', '')
end
diff --git a/db/init_structure.sql b/db/init_structure.sql
index 0883ef40d82..00f06078426 100644
--- a/db/init_structure.sql
+++ b/db/init_structure.sql
@@ -10,8 +10,6 @@ CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-CREATE EXTENSION IF NOT EXISTS plpgsql;
-
CREATE FUNCTION set_has_external_issue_tracker() RETURNS trigger
LANGUAGE plpgsql
AS $$
diff --git a/db/structure.sql b/db/structure.sql
index 40c2ec248d2..4345733c761 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10,8 +10,6 @@ CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-CREATE EXTENSION IF NOT EXISTS plpgsql;
-
CREATE FUNCTION delete_associated_project_namespace() RETURNS trigger
LANGUAGE plpgsql
AS $$
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 0b5d73afacd..5c7a53c3089 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -17449,6 +17449,9 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindingdescription"></a>`description` | [`String`](#string) | Description of the vulnerability finding. |
| <a id="pipelinesecurityreportfindingdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="pipelinesecurityreportfindingdetails"></a>`details` | [`[VulnerabilityDetail!]!`](#vulnerabilitydetail) | Details of the security finding. |
+| <a id="pipelinesecurityreportfindingdismissalreason"></a>`dismissalReason` | [`VulnerabilityDismissalReason`](#vulnerabilitydismissalreason) | Reason for the dismissal of the security report finding. |
+| <a id="pipelinesecurityreportfindingdismissedat"></a>`dismissedAt` | [`Time`](#time) | Time of the dismissal of the security report finding. |
+| <a id="pipelinesecurityreportfindingdismissedby"></a>`dismissedBy` | [`UserCore`](#usercore) | User who dismissed the security report finding. |
| <a id="pipelinesecurityreportfindingevidence"></a>`evidence` | [`VulnerabilityEvidence`](#vulnerabilityevidence) | Evidence for the vulnerability. |
| <a id="pipelinesecurityreportfindingfalsepositive"></a>`falsePositive` | [`Boolean`](#boolean) | Indicates whether the vulnerability is a false positive. |
| <a id="pipelinesecurityreportfindingidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerability finding. |
@@ -17465,6 +17468,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindingseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability finding. |
| <a id="pipelinesecurityreportfindingsolution"></a>`solution` | [`String`](#string) | Solution for resolving the security report finding. |
| <a id="pipelinesecurityreportfindingstate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | Finding status. |
+| <a id="pipelinesecurityreportfindingstatecomment"></a>`stateComment` | [`String`](#string) | Comment for the state of the security report finding. |
| <a id="pipelinesecurityreportfindingtitle"></a>`title` | [`String`](#string) | Title of the vulnerability finding. |
| <a id="pipelinesecurityreportfindinguuid"></a>`uuid` | [`String`](#string) | UUIDv5 digest based on the vulnerability's report type, primary identifier, location, fingerprint, project identifier. |
| <a id="pipelinesecurityreportfindingvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability related to the security report finding. |
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index a3906990402..1b39b74a23b 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -723,6 +723,7 @@ Enterprise Edition instance. This has some implications:
1. Try to avoid that, and add to `ApplicationSetting` instead.
1. Ensure that it is also
[added to Omnibus](https://docs.gitlab.com/omnibus/settings/gitlab.yml#adding-a-new-setting-to-gitlabyml).
+ 1. Ensure that it is also [added to Charts](https://docs.gitlab.com/charts/development/style_guide.html), if needed.
1. **File system access** is not possible in a [cloud-native architecture](architecture.md#adapting-existing-and-introducing-new-components).
Ensure that we support object storage for any file storage we need to perform. For more
information, see the [uploads documentation](uploads/index.md).
diff --git a/doc/raketasks/restore_gitlab.md b/doc/raketasks/restore_gitlab.md
index c97a71b1b5d..c5bbb38cc37 100644
--- a/doc/raketasks/restore_gitlab.md
+++ b/doc/raketasks/restore_gitlab.md
@@ -44,10 +44,6 @@ or `/home/git/gitlab/config/gitlab.yml` (for installations from source) and
any TLS keys, certificates (`/etc/gitlab/ssl`, `/etc/gitlab/trusted-certs`), or
[SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
-Starting with GitLab 12.9, if an untarred backup (like the ones made with
-`SKIP=tar`) is found, and no backup is chosen with `BACKUP=<timestamp>`, the
-untarred backup is used.
-
Depending on your case, you might want to run the restore command with one or
more of the following options:
@@ -383,3 +379,20 @@ For example, to restore all repositories for all projects in **Group A** (`group
```shell
sudo -u git -H bundle exec rake gitlab:backup:restore BACKUP=timestamp_of_backup REPOSITORIES_PATHS=group-a,group-b/project-c
```
+
+### Restore untarred backups
+
+If an [untarred backup](backup_gitlab.md#skipping-tar-creation) (made with `SKIP=tar`) is found,
+and no backup is chosen with `BACKUP=<timestamp>`, the untarred backup is used.
+
+For example, for Omnibus GitLab installations:
+
+```shell
+sudo gitlab-backup restore
+```
+
+For example, for installations from source:
+
+```shell
+sudo -u git -H bundle exec rake gitlab:backup:restore
+```
diff --git a/doc/user/project/issues/create_issues.md b/doc/user/project/issues/create_issues.md
index 18adce313d3..5ebb2fc2e1c 100644
--- a/doc/user/project/issues/create_issues.md
+++ b/doc/user/project/issues/create_issues.md
@@ -127,7 +127,7 @@ You can send an email to create an issue in a project on the project's
Prerequisites:
- Your GitLab instance must have [incoming email](../../../administration/incoming_email.md)
- configured.
+ configured with [email sub-addressing or catch-all mailbox](../../../administration/incoming_email.md#requirements).
- There must be at least one issue in the issue list.
- You must have at least the Guest role for the project.
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
index de3ff4ef6e5..d67c595512a 100644
--- a/doc/user/shortcuts.md
+++ b/doc/user/shortcuts.md
@@ -333,6 +333,13 @@ To disable keyboard shortcuts:
press <kbd>?</kbd> to display the list of shortcuts.
1. Select **Toggle shortcuts**.
+## Enable keyboard shortcuts
+
+To enable keyboard shortcuts:
+
+1. On the top bar, select the Help menu (**{question}**), then **Keyboard shortcuts**.
+1. Select **Toggle shortcuts**.
+
## Troubleshooting
### Linux shortcuts
diff --git a/lib/gitlab/ci/build/auto_retry.rb b/lib/gitlab/ci/build/auto_retry.rb
index 4950a7616c8..453c293f6cd 100644
--- a/lib/gitlab/ci/build/auto_retry.rb
+++ b/lib/gitlab/ci/build/auto_retry.rb
@@ -47,7 +47,9 @@ class Gitlab::Ci::Build::AutoRetry
end
def options_retry_when
- options_retry.fetch(:when, ['always'])
+ default = ['always']
+
+ options_retry.fetch(:when, default) || default
end
def retry_on_reason_or_always?
diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb
new file mode 100644
index 00000000000..010ce57d2a0
--- /dev/null
+++ b/lib/gitlab/ci/components/instance_path.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Components
+ class InstancePath
+ include Gitlab::Utils::StrongMemoize
+
+ def self.match?(address)
+ address.include?('@') && address.start_with?(Settings.gitlab_ci['component_fqdn'])
+ end
+
+ attr_reader :host
+
+ def initialize(address:, content_filename:)
+ @full_path, @version = address.to_s.split('@', 2)
+ @content_filename = content_filename
+ @host = Settings.gitlab_ci['component_fqdn']
+ end
+
+ def fetch_content!(current_user:)
+ return unless project
+ return unless sha
+
+ raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project)
+
+ project.repository.blob_data_at(sha, project_file_path)
+ end
+
+ def project
+ find_project_by_component_path(instance_path)
+ end
+ strong_memoize_attr :project
+
+ def project_file_path
+ return unless project
+
+ component_dir = instance_path.delete_prefix(project.full_path)
+ File.join(component_dir, @content_filename).delete_prefix('/')
+ end
+
+ # TODO: Add support when version is a released tag and "~latest" moving target
+ def sha
+ return unless project
+
+ project.commit(version)&.id
+ end
+ strong_memoize_attr :sha
+
+ private
+
+ attr_reader :version, :path
+
+ def instance_path
+ @full_path.delete_prefix(host)
+ end
+
+ # Given a path like "my-org/sub-group/the-project/path/to/component"
+ # find the project "my-org/sub-group/the-project" by looking at all possible paths.
+ def find_project_by_component_path(path)
+ possible_paths = [path]
+
+ while index = path.rindex('/') # find index of last `/` in a path
+ possible_paths << (path = path[0..index - 1])
+ end
+
+ # remove shortest path as it is group
+ possible_paths.pop
+
+ ::Project.where_full_path_in(possible_paths).take # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/include.rb b/lib/gitlab/ci/config/entry/include.rb
index 368d8f07f8d..baab7ebdb79 100644
--- a/lib/gitlab/ci/config/entry/include.rb
+++ b/lib/gitlab/ci/config/entry/include.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[local file remote template artifact job project ref rules].freeze
+ ALLOWED_KEYS = %i[local file remote template component artifact job project ref rules].freeze
validations do
validates :config, hash_or_string: true
@@ -36,8 +36,8 @@ module Gitlab
end
entry :rules, ::Gitlab::Ci::Config::Entry::Include::Rules,
- description: 'List of evaluable Rules to determine file inclusion.',
- inherit: false
+ description: 'List of evaluable Rules to determine file inclusion.',
+ inherit: false
attributes :rules
diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb
new file mode 100644
index 00000000000..33e7724bf9b
--- /dev/null
+++ b/lib/gitlab/ci/config/external/file/component.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module External
+ module File
+ class Component < Base
+ extend ::Gitlab::Utils::Override
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(params, context)
+ @location = params[:component]
+ super
+ end
+
+ def matching?
+ super && ::Feature.enabled?(:ci_include_components, context.project)
+ end
+
+ def content
+ return unless component_result.success?
+
+ component_result.payload.fetch(:content)
+ end
+ strong_memoize_attr :content
+
+ def metadata
+ super.merge(
+ type: :component,
+ location: masked_location,
+ blob: masked_blob,
+ raw: nil,
+ extra: {}
+ )
+ end
+
+ def validate_location!
+ return unless invalid_location_type?
+
+ errors.push("Included file `#{masked_location}` needs to be a string")
+ end
+
+ def validate_context!
+ return if context.project&.repository
+
+ errors.push('Unable to use components outside of a project context')
+ end
+
+ def validate_content!
+ return if content.present?
+
+ errors.push(component_result.message)
+ end
+
+ private
+
+ attr_reader :path, :version
+
+ def component_result
+ ::Ci::Components::FetchService.new(
+ address: location,
+ current_user: context.user
+ ).execute
+ end
+ strong_memoize_attr :component_result
+
+ override :expand_context_attrs
+ def expand_context_attrs
+ {
+ project: component_path.project,
+ sha: component_path.sha,
+ user: context.user,
+ variables: context.variables
+ }
+ end
+
+ def masked_blob
+ return unless component_path
+
+ context.mask_variables_from(
+ Gitlab::Routing.url_helpers.project_blob_url(
+ component_path.project,
+ ::File.join(component_path.sha, component_path.project_file_path))
+ )
+ end
+ strong_memoize_attr :masked_blob
+
+ def component_path
+ return unless component_result.success?
+
+ component_result.payload.fetch(:path)
+ end
+ strong_memoize_attr :component_path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/external/mapper/matcher.rb b/lib/gitlab/ci/config/external/mapper/matcher.rb
index 85e19ff1ced..e59eaa6d324 100644
--- a/lib/gitlab/ci/config/external/mapper/matcher.rb
+++ b/lib/gitlab/ci/config/external/mapper/matcher.rb
@@ -10,6 +10,7 @@ module Gitlab
FILE_CLASSES = [
External::File::Local,
External::File::Project,
+ External::File::Component,
External::File::Remote,
External::File::Template,
External::File::Artifact
@@ -29,11 +30,11 @@ module Gitlab
matching.first
elsif matching.empty?
raise Mapper::AmbigiousSpecificationError,
- "`#{masked_location(location.to_json)}` does not have a valid subkey for include. " \
- "Valid subkeys are: `#{FILE_SUBKEYS.join('`, `')}`"
+ "`#{masked_location(location.to_json)}` does not have a valid subkey for include. " \
+ "Valid subkeys are: `#{FILE_SUBKEYS.join('`, `')}`"
else
raise Mapper::AmbigiousSpecificationError,
- "Each include must use only one of: `#{FILE_SUBKEYS.join('`, `')}`"
+ "Each include must use only one of: `#{FILE_SUBKEYS.join('`, `')}`"
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb
index b17ae77d445..b886aa22ba3 100644
--- a/lib/gitlab/ci/pipeline/chain/metrics.rb
+++ b/lib/gitlab/ci/pipeline/chain/metrics.rb
@@ -6,15 +6,27 @@ module Gitlab
module Chain
class Metrics < Chain::Base
def perform!
- counter.increment(source: @pipeline.source)
+ increment_pipeline_created_counter
+ create_snowplow_event_for_pipeline_name
end
def break?
false
end
- def counter
- ::Gitlab::Ci::Pipeline::Metrics.pipelines_created_counter
+ def increment_pipeline_created_counter
+ ::Gitlab::Ci::Pipeline::Metrics.pipelines_created_counter.increment(source: @pipeline.source)
+ end
+
+ def create_snowplow_event_for_pipeline_name
+ return unless @pipeline.pipeline_metadata&.name
+
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create_pipeline_with_name',
+ project: @pipeline.project,
+ user: @pipeline.user,
+ namespace: @pipeline.project.namespace)
end
end
end
diff --git a/lib/gitlab/ci/reports/sbom/component.rb b/lib/gitlab/ci/reports/sbom/component.rb
index 5188304f4ed..08307580987 100644
--- a/lib/gitlab/ci/reports/sbom/component.rb
+++ b/lib/gitlab/ci/reports/sbom/component.rb
@@ -31,8 +31,10 @@ module Gitlab
end
def supported_purl_type?
+ # the purl type is not required as per the spec: https://cyclonedx.org/docs/1.4/json/#components_items_purl
return true unless purl
+ # however, if the purl type is provided, it _must be valid_
::Enums::Sbom.purl_types.include?(purl.type.to_sym)
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 66ffaf0922c..f267026ea44 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7076,6 +7076,12 @@ msgstr ""
msgid "BranchRules|%{total} status %{subject}"
msgstr ""
+msgid "BranchRules|Add branch rule"
+msgstr ""
+
+msgid "BranchRules|After a protected branch is created, it will show up in the list as a branch rule."
+msgstr ""
+
msgid "BranchRules|All branches"
msgstr ""
@@ -7121,6 +7127,9 @@ msgstr ""
msgid "BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
+msgid "BranchRules|Create protected branch"
+msgstr ""
+
msgid "BranchRules|Create wildcard: %{searchTerm}"
msgstr ""
@@ -7184,6 +7193,9 @@ msgstr ""
msgid "BranchRules|Target branch"
msgstr ""
+msgid "BranchRules|To create a branch rule, you first need to create a protected branch."
+msgstr ""
+
msgid "BranchRules|Users"
msgstr ""
@@ -34316,6 +34328,9 @@ msgstr ""
msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
msgstr ""
+msgid "ProtectedBranch|After you configure a protected branch, merge request approval, or status check, it appears here."
+msgstr ""
+
msgid "ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}."
msgstr ""
@@ -34400,9 +34415,6 @@ msgstr ""
msgid "ProtectedBranch|Protected branches"
msgstr ""
-msgid "ProtectedBranch|Protected branches, merge request approvals, and status checks will appear here once configured."
-msgstr ""
-
msgid "ProtectedBranch|Protected tags (%{tags_count})"
msgstr ""
@@ -46520,6 +46532,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Value stream"
msgstr ""
+msgid "ValueStreamAnalytics|View details"
+msgstr ""
+
msgid "ValueStreamEvent|Items in stage"
msgstr ""
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 4917b043812..1aa1d885be3 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Settings, feature_category: :authentication_and_authorization do
+ using RSpec::Parameterized::TableSyntax
+
describe 'omniauth' do
it 'defaults to enabled' do
expect(described_class.omniauth.enabled).to be true
@@ -15,6 +17,32 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do
end
end
+ describe '.build_ci_component_fqdn' do
+ subject(:fqdn) { described_class.build_ci_component_fqdn }
+
+ where(:host, :port, :relative_url, :result) do
+ 'acme.com' | 9090 | '/gitlab' | 'acme.com:9090/gitlab/'
+ 'acme.com' | 443 | '/gitlab' | 'acme.com/gitlab/'
+ 'acme.com' | 443 | '' | 'acme.com/'
+ 'acme.com' | 9090 | '' | 'acme.com:9090/'
+ 'test' | 9090 | '' | 'test:9090/'
+ end
+
+ with_them do
+ before do
+ allow(Gitlab.config).to receive(:gitlab).and_return(
+ Settingslogic.new({
+ 'host' => host,
+ 'https' => true,
+ 'port' => port,
+ 'relative_url_root' => relative_url
+ }))
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
describe '.attr_encrypted_db_key_base_truncated' do
it 'is a string with maximum 32 bytes size' do
expect(described_class.attr_encrypted_db_key_base_truncated.bytesize)
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index 6a58f8c6d29..e0bfff3e664 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -1,6 +1,7 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
+import { METRIC_POPOVER_LABEL } from '~/analytics/shared/constants';
const MOCK_METRIC = {
key: 'deployment-frequency',
@@ -27,10 +28,11 @@ describe('MetricPopover', () => {
};
const findMetricLabel = () => wrapper.findByTestId('metric-label');
- const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
+ const findMetricLink = () => wrapper.find('[data-testid="metric-link"]');
const findMetricDescription = () => wrapper.findByTestId('metric-description');
const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
+ const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
@@ -47,17 +49,14 @@ describe('MetricPopover', () => {
});
describe('with links', () => {
+ const METRIC_NAME = 'Deployment frequency';
+ const LINK_URL = '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency';
const links = [
{
- name: 'Deployment frequency',
- url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency',
+ name: METRIC_NAME,
+ url: LINK_URL,
label: 'Dashboard',
},
- {
- name: 'Another link',
- url: '/groups/gitlab-org/-/analytics/another-link',
- label: 'Another link',
- },
];
const docsLink = {
name: 'Deployment frequency',
@@ -68,37 +67,34 @@ describe('MetricPopover', () => {
const linksWithDocs = [...links, docsLink];
describe.each`
- hasDocsLink | allLinks | displayedMetricLinks
- ${true} | ${linksWithDocs} | ${links}
- ${false} | ${links} | ${links}
- `(
- 'when one link has docs_link=$hasDocsLink',
- ({ hasDocsLink, allLinks, displayedMetricLinks }) => {
- beforeEach(() => {
- wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
- });
+ hasDocsLink | allLinks
+ ${true} | ${linksWithDocs}
+ ${false} | ${links}
+ `('when one link has docs_link=$hasDocsLink', ({ hasDocsLink, allLinks }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
+ });
- displayedMetricLinks.forEach((link, idx) => {
- it(`renders a link for "${link.name}"`, () => {
- const allLinkContainers = findAllMetricLinks();
+ describe('Metric title row', () => {
+ it(`renders a link for "${METRIC_NAME}"`, () => {
+ expect(findMetricLink().text()).toContain(METRIC_POPOVER_LABEL);
+ expect(findMetricLink().findComponent(GlLink).attributes('href')).toBe(LINK_URL);
+ });
- expect(allLinkContainers.at(idx).text()).toContain(link.name);
- expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe(
- link.url,
- );
- });
+ it('renders the chart icon', () => {
+ expect(findMetricDetailsIcon().attributes('name')).toBe('chart');
});
+ });
- it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
- expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
+ it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
+ expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
- if (hasDocsLink) {
- expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
- expect(findMetricDocsLink().text()).toBe(docsLink.label);
- expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
- }
- });
- },
- );
+ if (hasDocsLink) {
+ expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
+ expect(findMetricDocsLink().text()).toBe(docsLink.label);
+ expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
+ }
+ });
+ });
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 447d7e86ceb..56b39f04580 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -1,9 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
+import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
@@ -11,8 +12,19 @@ import {
branchRulesMockResponse,
appProvideMock,
} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
+import {
+ I18N,
+ BRANCH_PROTECTION_MODAL_ID,
+ PROTECTED_BRANCHES_ANCHOR,
+} from '~/projects/settings/repository/branch_rules/constants';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { expandSection } from '~/settings_panels';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
+jest.mock('~/settings_panels');
+jest.mock('~/lib/utils/common_utils');
Vue.use(VueApollo);
@@ -28,6 +40,8 @@ describe('Branch rules app', () => {
wrapper = mountExtended(BranchRules, {
apolloProvider: fakeApollo,
provide: appProvideMock,
+ stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
+ directives: { GlModal: createMockDirective() },
});
await waitForPromises();
@@ -35,17 +49,19 @@ describe('Branch rules app', () => {
const findAllBranchRules = () => wrapper.findAllComponents(BranchRule);
const findEmptyState = () => wrapper.findByTestId('empty');
+ const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule);
+ const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => createComponent());
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError });
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(findEmptyState().text()).toBe(i18n.emptyState);
+ expect(findEmptyState().text()).toBe(I18N.emptyState);
});
it('renders branch rules', () => {
@@ -61,4 +77,38 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
+
+ describe('Add branch rule', () => {
+ it('renders an Add branch rule button', () => {
+ expect(findAddBranchRuleButton().exists()).toBe(true);
+ });
+
+ it('renders a modal with correct props/attributes', () => {
+ expect(findModal().props()).toMatchObject({
+ modalId: BRANCH_PROTECTION_MODAL_ID,
+ title: I18N.addBranchRule,
+ });
+
+ expect(findModal().attributes('ok-title')).toBe(I18N.createProtectedBranch);
+ });
+
+ it('renders correct modal id for the default action', () => {
+ const binding = getBinding(findAddBranchRuleButton().element, 'gl-modal');
+
+ expect(binding.value).toBe(BRANCH_PROTECTION_MODAL_ID);
+ });
+
+ it('renders the correct modal content', () => {
+ expect(findModal().text()).toContain(I18N.branchRuleModalDescription);
+ expect(findModal().text()).toContain(I18N.branchRuleModalContent);
+ });
+
+ it('when the primary modal action is clicked, takes user to the correct location', () => {
+ findAddBranchRuleButton().trigger('click');
+ findModal().vm.$emit('ok');
+
+ expect(expandSection).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ expect(scrollToElement).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index d69b6679e30..314714c543b 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -112,5 +112,13 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authori
expect(result).to eq ['always']
end
end
+
+ context 'with retry[:when] set to nil' do
+ let(:build) { create(:ci_build, options: { retry: { when: nil } }) }
+
+ it 'returns always array' do
+ expect(result).to eq ['always']
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
new file mode 100644
index 00000000000..d9beae0555c
--- /dev/null
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do
+ let_it_be(:user) { create(:user) }
+
+ let(:path) { described_class.new(address: address, content_filename: 'template.yml') }
+ let(:settings) { Settingslogic.new({ 'component_fqdn' => current_host }) }
+ let(:current_host) { 'acme.com/' }
+
+ before do
+ allow(::Settings).to receive(:gitlab_ci).and_return(settings)
+ end
+
+ describe 'FQDN path' do
+ let_it_be(:existing_project) { create(:project, :repository) }
+
+ let(:project_path) { existing_project.full_path }
+ let(:address) { "acme.com/#{project_path}/component@#{version}" }
+ let(:version) { 'master' }
+
+ context 'when project exists' do
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+
+ context 'when content exists' do
+ let(:content) { 'image: alpine' }
+
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance)
+ .to receive(:blob_data_at)
+ .with(existing_project.commit('master').id, 'component/template.yml')
+ .and_return(content)
+ end
+ end
+
+ context 'when user has permissions to read code' do
+ before do
+ existing_project.add_developer(user)
+ end
+
+ it 'fetches the content' do
+ expect(path.fetch_content!(current_user: user)).to eq(content)
+ end
+ end
+
+ context 'when user does not have permissions to download code' do
+ it 'raises an error when fetching the content' do
+ expect { path.fetch_content!(current_user: user) }
+ .to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+ end
+
+ context 'when project path is nested under a subgroup' do
+ let(:existing_group) { create(:group, :nested) }
+ let(:existing_project) { create(:project, :repository, group: existing_group) }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+ end
+
+ context 'when current GitLab instance is installed on a relative URL' do
+ let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" }
+ let(:current_host) { 'acme.com/gitlab/' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+ end
+
+ context 'when version does not exist' do
+ let(:version) { 'non-existent' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to be_nil
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+
+ it 'returns nil when fetching the content' do
+ expect(path.fetch_content!(current_user: user)).to be_nil
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:project_path) { 'non-existent/project' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to be_nil
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to be_nil
+ expect(path.project_file_path).to be_nil
+ end
+
+ it 'returns nil when fetching the content' do
+ expect(path.fetch_content!(current_user: user)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb
index fd7f85c9298..5eecff5b592 100644
--- a/spec/lib/gitlab/ci/config/entry/include_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb
@@ -44,6 +44,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
it { is_expected.to be_valid }
end
+ context 'when using "component"' do
+ let(:config) { { component: 'path/to/component@1.0' } }
+
+ it { is_expected.to be_valid }
+ end
+
context 'when using "artifact"' do
context 'and specifying "job"' do
let(:config) { { artifact: 'test.yml', job: 'generator' } }
diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
new file mode 100644
index 00000000000..9863941c370
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do
+ let_it_be(:context_project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_variables) { project.predefined_variables }
+
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
+ let(:external_resource) { described_class.new(params, context) }
+ let(:params) { { component: 'gitlab.com/acme/components/my-component@1.0' } }
+ let(:fetch_service) { instance_double(::Ci::Components::FetchService) }
+ let(:response) { ServiceResponse.error(message: 'some error message') }
+
+ let(:context_params) do
+ {
+ project: context_project,
+ sha: '12345',
+ user: user,
+ variables: project_variables
+ }
+ end
+
+ before do
+ allow(::Ci::Components::FetchService)
+ .to receive(:new)
+ .with(
+ address: params[:component],
+ current_user: context.user
+ ).and_return(fetch_service)
+
+ allow(fetch_service).to receive(:execute).and_return(response)
+ end
+
+ describe '#matching?' do
+ subject(:matching) { external_resource.matching? }
+
+ context 'when component is specified' do
+ let(:params) { { component: 'some-value' } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when feature flag ci_include_components is disabled' do
+ before do
+ stub_feature_flags(ci_include_components: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when component is not specified' do
+ let(:params) { { local: 'some-value' } }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#valid?' do
+ subject(:valid?) do
+ external_resource.validate!
+ external_resource.valid?
+ end
+
+ context 'when the context project does not have a repository' do
+ before do
+ allow(context_project).to receive(:repository).and_return(nil)
+ end
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Unable to use components outside of a project context')
+ end
+ end
+
+ context 'when location is not provided' do
+ let(:params) { { component: 123 } }
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Included file `123` needs to be a string')
+ end
+ end
+
+ context 'when component path is provided' do
+ context 'when component is not found' do
+ let(:response) do
+ ServiceResponse.error(message: 'Content not found')
+ end
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Content not found')
+ end
+ end
+
+ context 'when component is found' do
+ let(:content) do
+ <<~COMPONENT
+ job:
+ script: echo
+ COMPONENT
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: {
+ content: content,
+ path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345')
+ })
+ end
+
+ it 'is valid' do
+ expect(subject).to be_truthy
+ expect(external_resource.content).to eq(content)
+ end
+
+ context 'when content is not a valid YAML' do
+ let(:content) { 'the-content' }
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to match(/does not have valid YAML syntax/)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#metadata' do
+ subject(:metadata) { external_resource.metadata }
+
+ let(:component_path) do
+ instance_double(::Gitlab::Ci::Components::InstancePath,
+ project: project,
+ sha: '12345',
+ project_file_path: 'my-component/template.yml')
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: { path: component_path })
+ end
+
+ it 'returns the metadata' do
+ is_expected.to include(
+ context_project: context_project.full_path,
+ context_sha: context.sha,
+ type: :component,
+ location: 'gitlab.com/acme/components/my-component@1.0',
+ blob: a_string_ending_with("#{project.full_path}/-/blob/12345/my-component/template.yml"),
+ raw: nil,
+ extra: {}
+ )
+ end
+ end
+
+ describe '#expand_context' do
+ let(:component_path) do
+ instance_double(::Gitlab::Ci::Components::InstancePath,
+ project: project,
+ sha: '12345')
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: { path: component_path })
+ end
+
+ subject { external_resource.send(:expand_context_attrs) }
+
+ it 'inherits user and variables while changes project and sha' do
+ is_expected.to include(
+ project: project,
+ sha: '12345',
+ user: context.user,
+ variables: context.variables)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
index 5f321a696c9..11c79e19cff 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
@@ -17,11 +17,14 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
describe '#process' do
let(:locations) do
- [{ local: 'file.yml' },
- { file: 'file.yml', project: 'namespace/project' },
- { remote: 'https://example.com/.gitlab-ci.yml' },
- { template: 'file.yml' },
- { artifact: 'generated.yml', job: 'test' }]
+ [
+ { local: 'file.yml' },
+ { file: 'file.yml', project: 'namespace/project' },
+ { component: 'gitlab.com/org/component@1.0' },
+ { remote: 'https://example.com/.gitlab-ci.yml' },
+ { template: 'file.yml' },
+ { artifact: 'generated.yml', job: 'test' }
+ ]
end
subject(:process) { matcher.process(locations) }
@@ -30,6 +33,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
is_expected.to contain_exactly(
an_instance_of(Gitlab::Ci::Config::External::File::Local),
an_instance_of(Gitlab::Ci::Config::External::File::Project),
+ an_instance_of(Gitlab::Ci::Config::External::File::Component),
an_instance_of(Gitlab::Ci::Config::External::File::Remote),
an_instance_of(Gitlab::Ci::Config::External::File::Template),
an_instance_of(Gitlab::Ci::Config::External::File::Artifact)
@@ -42,8 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
it 'raises an error' do
expect { process }.to raise_error(
Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
- '`{"invalid":"file.yml"}` does not have a valid subkey for include. ' \
- 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`'
+ /`{"invalid":"file.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
@@ -53,8 +56,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
it 'raises an error with a masked sentence' do
expect { process }.to raise_error(
Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
- '`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. ' \
- 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`'
+ /`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
end
@@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
it 'raises an error' do
expect { process }.to raise_error(
Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
- "Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`"
+ /Each include must use only one of:/
)
end
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 344e9095fab..a053c3047de 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -124,7 +124,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do |ci_batch_request_for
end
it 'returns ambigious specification error' do
- expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, '`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`')
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are:/)
end
end
@@ -138,7 +138,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do |ci_batch_request_for
end
it 'returns ambigious specification error' do
- expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`')
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /Each include must use only one of/)
end
end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 311b433b7d2..bb65c2ef10c 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -400,6 +400,44 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
end
end
+ describe 'include:component' do
+ let(:values) do
+ {
+ include: { component: "#{Gitlab.config.gitlab.host}/#{another_project.full_path}/component-x@master" },
+ image: 'image:1.0'
+ }
+ end
+
+ let(:other_project_files) do
+ {
+ '/component-x/template.yml' => <<~YAML
+ component_x_job:
+ script: echo Component X
+ YAML
+ }
+ end
+
+ before do
+ another_project.add_developer(user)
+ end
+
+ it 'appends the file to the values' do
+ output = processor.perform
+ expect(output.keys).to match_array([:image, :component_x_job])
+ end
+
+ context 'when feature flag ci_include_components is disabled' do
+ before do
+ stub_feature_flags(ci_include_components: false)
+ end
+
+ it 'returns an error' do
+ expect { processor.perform }
+ .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./)
+ end
+ end
+ end
+
context 'when a valid project file is defined' do
let(:values) do
{
diff --git a/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb
new file mode 100644
index 00000000000..b955d0e7cee
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Metrics, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'master', user: user, name: 'Build pipeline')
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ origin_ref: 'master')
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ subject(:run_chain) { step.perform! }
+
+ it 'does not break the chain' do
+ run_chain
+
+ expect(step.break?).to be false
+ end
+
+ context 'with pipeline name' do
+ it 'creates snowplow event' do
+ run_chain
+
+ expect_snowplow_event(
+ category: described_class.to_s,
+ action: 'create_pipeline_with_name',
+ project: pipeline.project,
+ user: pipeline.user,
+ namespace: pipeline.project.namespace
+ )
+ end
+ end
+
+ context 'without pipeline name' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'master', user: user)
+ end
+
+ it 'does not create snowplow event' do
+ run_chain
+
+ expect_no_snowplow_event
+ end
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a1fd51f60ea..917c0d33183 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -27,6 +27,14 @@ RSpec.describe Ci::JobArtifact do
subject { build(:ci_job_artifact, :archive, job: job, size: 107464) }
end
+ describe 'after_destroy callback' do
+ it 'logs the job artifact destroy' do
+ expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_deleted).with(artifact, :log_destroy)
+
+ artifact.destroy!
+ end
+ end
+
describe '.not_expired' do
it 'returns artifacts that have not expired' do
_expired_artifact = create(:ci_job_artifact, :expired)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 0d53ebdefe9..23de9a50e50 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -62,6 +62,33 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
expect(environment).not_to be_valid
end
end
+
+ context 'tier' do
+ let!(:env) { build(:environment, tier: nil) }
+
+ before do
+ # Disable `before_validation: :ensure_environment_tier` since it always set tier and interfere with tests.
+ # See: https://github.com/thoughtbot/shoulda/issues/178#issuecomment-1654014
+
+ allow_any_instance_of(described_class).to receive(:ensure_environment_tier).and_return(env)
+ end
+
+ context 'presence is checked' do
+ it 'during create and update' do
+ expect(env).to validate_presence_of(:tier).on(:create)
+ expect(env).to validate_presence_of(:tier).on(:update)
+ end
+ end
+
+ context 'when FF is disabled' do
+ before do
+ stub_feature_flags(validate_environment_tier_presence: false)
+ end
+
+ it { expect(env).to validate_presence_of(:tier).on(:create) }
+ it { expect(env).not_to validate_presence_of(:tier).on(:update) }
+ end
+ end
end
describe 'preloading deployment associations' do
@@ -145,7 +172,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
environment = create(:environment, name: 'gprd')
environment.update_column(:tier, nil)
- expect { environment.stop! }.to change { environment.reload.tier }.from(nil).to('production')
+ expect { environment.save! }.to change { environment.reload.tier }.from(nil).to('production')
end
it 'does not overwrite the existing environment tier' do
diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb
new file mode 100644
index 00000000000..f2eaa8d31b4
--- /dev/null
+++ b/spec/services/ci/components/fetch_service_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do
+ let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { user }
+ let_it_be(:current_host) { Gitlab.config.gitlab.host }
+
+ let(:service) do
+ described_class.new(address: address, current_user: current_user)
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute', :aggregate_failures do
+ subject(:result) { service.execute }
+
+ shared_examples 'an external component' do
+ shared_examples 'component address' do
+ context 'when content exists' do
+ let(:sha) { project.commit(version).id }
+
+ let(:content) do
+ <<~COMPONENT
+ job:
+ script: echo
+ COMPONENT
+ end
+
+ before do
+ stub_project_blob(sha, component_yaml_path, content)
+ end
+
+ it 'returns the content' do
+ expect(result).to be_success
+ expect(result.payload[:content]).to eq(content)
+ end
+ end
+
+ context 'when content does not exist' do
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+ end
+
+ context 'when user does not have permissions to read the code' do
+ let(:version) { 'master' }
+ let(:current_user) { create(:user) }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:not_allowed)
+ end
+ end
+
+ context 'when version is a branch name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.default_branch }
+ end
+ end
+
+ context 'when version is a tag name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.name }
+ end
+ end
+
+ context 'when version is a commit sha' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.id }
+ end
+ end
+
+ context 'when version is not provided' do
+ let(:version) { nil }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:component_path) { 'unknown/component' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when host is different than the current instance host' do
+ let(:current_host) { 'another-host.com' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:unsupported_path)
+ end
+ end
+ end
+
+ context 'when address points to an external component' do
+ let(:address) { "#{current_host}/#{component_path}@#{version}" }
+
+ context 'when component path is the full path to a project' do
+ let(:component_path) { project.full_path }
+ let(:component_yaml_path) { 'template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-component" }
+ let(:component_yaml_path) { 'my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a nested directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-dir/my-component" }
+ let(:component_yaml_path) { 'my-dir/my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+ end
+ end
+
+ def stub_project_blob(ref, path, content)
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content)
+ end
+ end
+end