summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/authentication/password/components/password_input.vue43
-rw-r--r--app/assets/javascripts/authentication/password/constants.js6
-rw-r--r--app/assets/javascripts/authentication/password/index.js47
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue1
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue1
-rw-r--r--app/assets/javascripts/profile/components/user_achievements.vue42
-rw-r--r--app/graphql/types/achievements/achievement_type.rb1
-rw-r--r--app/mailers/emails/service_desk.rb72
-rw-r--r--app/models/commit_collection.rb4
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/packages/npm/metadata_cache.rb27
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/services/ml/experiment_tracking/candidate_repository.rb2
-rw-r--r--app/services/packages/npm/create_metadata_cache_service.rb53
-rw-r--r--app/uploaders/packages/npm/metadata_cache_uploader.rb31
-rw-r--r--app/views/devise/shared/_signup_box.html.haml7
-rw-r--r--app/views/registrations/welcome/show.html.haml2
-rw-r--r--config/feature_flags/development/always_perform_delayed_deletion.yml2
-rw-r--r--config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml2
-rw-r--r--config/feature_flags/development/use_traversal_ids_groups_finder.yml2
-rw-r--r--db/migrate/20230403093349_ensure_packages_npm_metadata_caches_is_empty.rb13
-rw-r--r--db/migrate/20230405071033_add_object_storage_key_to_packages_npm_metadata_caches.rb25
-rw-r--r--db/migrate/20230503191056_add_text_limit_to_packages_npm_metadata_caches_object_storage_key.rb13
-rw-r--r--db/migrate/20230509072635_drop_unused_sequence_by_recreating_vsa_table.rb24
-rw-r--r--db/schema_migrations/202304030933491
-rw-r--r--db/schema_migrations/202304050710331
-rw-r--r--db/schema_migrations/202305031910561
-rw-r--r--db/schema_migrations/202305090726351
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/logs/index.md6
-rw-r--r--doc/api/discussions.md432
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/ci/services/index.md2
-rw-r--r--doc/ci/yaml/artifacts_reports.md14
-rw-r--r--doc/development/sec/token_revocation_api.md4
-rw-r--r--doc/install/docker.md7
-rw-r--r--doc/topics/gitlab_flow.md35
-rw-r--r--doc/user/application_security/secret_detection/automatic_response.md242
-rw-r--r--doc/user/application_security/secret_detection/index.md6
-rw-r--r--doc/user/application_security/secret_detection/post_processing.md245
-rw-r--r--doc/user/packages/gradle_repository/index.md375
-rw-r--r--doc/user/packages/maven_repository/index.md301
-rw-r--r--doc/user/profile/achievements.md5
-rw-r--r--lib/api/integrations.rb4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/product_analytics.yml2
-rw-r--r--lib/support/nginx/gitlab2
-rw-r--r--lib/support/nginx/gitlab-ssl2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/flow/user_onboarding.rb4
-rw-r--r--qa/qa/page/registration/welcome.rb4
-rw-r--r--scripts/review_apps/base-config.yaml6
-rw-r--r--spec/factories/packages/npm/metadata_cache.rb4
-rw-r--r--spec/fixtures/packages/npm/metadata.json20
-rw-r--r--spec/frontend/__mocks__/file_mock.js2
-rw-r--r--spec/frontend/authentication/password/components/password_input_spec.js23
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap2
-rw-r--r--spec/frontend/fixtures/users.rb10
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap4
-rw-r--r--spec/frontend/profile/components/user_achievements_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb56
-rw-r--r--spec/mailers/emails/service_desk_spec.rb255
-rw-r--r--spec/models/commit_collection_spec.rb18
-rw-r--r--spec/models/label_spec.rb11
-rw-r--r--spec/models/packages/npm/metadata_cache_spec.rb141
-rw-r--r--spec/policies/achievements/user_achievement_policy_spec.rb8
-rw-r--r--spec/policies/group_policy_spec.rb6
-rw-r--r--spec/requests/api/graphql/user/user_achievements_query_spec.rb7
-rw-r--r--spec/services/ml/experiment_tracking/candidate_repository_spec.rb10
-rw-r--r--spec/services/packages/npm/create_metadata_cache_service_spec.rb83
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb2
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb8
-rw-r--r--spec/uploaders/packages/npm/metadata_cache_uploader_spec.rb34
75 files changed, 1717 insertions, 1170 deletions
diff --git a/app/assets/javascripts/authentication/password/components/password_input.vue b/app/assets/javascripts/authentication/password/components/password_input.vue
index 7808620cca1..9b3c4a692a6 100644
--- a/app/assets/javascripts/authentication/password/components/password_input.vue
+++ b/app/assets/javascripts/authentication/password/components/password_input.vue
@@ -1,14 +1,9 @@
<script>
import { GlFormInput, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import { SHOW_PASSWORD, HIDE_PASSWORD, PASSWORD_TITLE } from '../constants';
+import { SHOW_PASSWORD, HIDE_PASSWORD } from '../constants';
export default {
name: 'PasswordInput',
- i18n: {
- showPassword: SHOW_PASSWORD,
- hidePassword: HIDE_PASSWORD,
- },
components: {
GlFormInput,
GlButton,
@@ -17,16 +12,33 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- resourceName: {
+ title: {
type: String,
- required: true,
+ required: false,
+ default: '',
+ },
+ id: {
+ type: String,
+ required: false,
+ default: '',
},
minimumPasswordLength: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
qaSelector: {
type: String,
+ required: false,
+ default: '',
+ },
+ autocomplete: {
+ type: String,
+ required: false,
+ default: 'current-password',
+ },
+ name: {
+ type: String,
required: true,
},
},
@@ -36,14 +48,11 @@ export default {
};
},
computed: {
- passwordTitle() {
- return sprintf(PASSWORD_TITLE, { minimum_password_length: this.minimumPasswordLength });
- },
type() {
return this.isMasked ? 'password' : 'text';
},
toggleVisibilityLabel() {
- return this.isMasked ? this.$options.i18n.showPassword : this.$options.i18n.hidePassword;
+ return this.isMasked ? SHOW_PASSWORD : HIDE_PASSWORD;
},
toggleVisibilityIcon() {
return this.isMasked ? 'eye' : 'eye-slash';
@@ -60,14 +69,14 @@ export default {
<template>
<div class="gl-field-error-anchor input-icon-wrapper">
<gl-form-input
- :id="`${resourceName}_password`"
+ :id="id"
class="js-password-complexity-validation gl-pr-8!"
required
- autocomplete="new-password"
- :name="`${resourceName}[password]`"
+ :autocomplete="autocomplete"
+ :name="name"
:minlength="minimumPasswordLength"
:data-qa-selector="qaSelector"
- :title="passwordTitle"
+ :title="title"
:type="type"
/>
<gl-button
diff --git a/app/assets/javascripts/authentication/password/constants.js b/app/assets/javascripts/authentication/password/constants.js
index 97e1a882d9d..da617877aec 100644
--- a/app/assets/javascripts/authentication/password/constants.js
+++ b/app/assets/javascripts/authentication/password/constants.js
@@ -1,8 +1,4 @@
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
export const SHOW_PASSWORD = __('Show password');
export const HIDE_PASSWORD = __('Hide password');
-
-export const PASSWORD_TITLE = s__(
- 'SignUp|Minimum length is %{minimum_password_length} characters.',
-);
diff --git a/app/assets/javascripts/authentication/password/index.js b/app/assets/javascripts/authentication/password/index.js
index 36e3b74263c..4a73e0975ca 100644
--- a/app/assets/javascripts/authentication/password/index.js
+++ b/app/assets/javascripts/authentication/password/index.js
@@ -3,30 +3,33 @@ import GlFieldErrors from '~/gl_field_errors';
import PasswordInput from './components/password_input.vue';
export const initTogglePasswordVisibility = () => {
- const el = document.querySelector('.js-password');
+ document.querySelectorAll('.js-password').forEach((el) => {
+ if (!el) {
+ return null;
+ }
- if (!el) {
- return null;
- }
+ const { form } = el;
+ const { title, id, minimumPasswordLength, qaSelector, autocomplete, name } = el.dataset;
- const { form } = el;
- const { resourceName, minimumPasswordLength, qaSelector } = el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'PasswordInputRoot',
+ render(createElement) {
+ return createElement(PasswordInput, {
+ props: {
+ title,
+ id,
+ minimumPasswordLength,
+ qaSelector,
+ autocomplete,
+ name,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el,
- name: 'PasswordInputRoot',
- render(createElement) {
- return createElement(PasswordInput, {
- props: {
- resourceName,
- minimumPasswordLength,
- qaSelector,
- },
- });
- },
+ // Since we replaced password input, we need to re-initialize the field errors handler
+ return new GlFieldErrors(form);
});
-
- // Since we replaced password input, we need to re-initialize the field errors handler
- return new GlFieldErrors(form);
};
diff --git a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
index ffd3a2caa7f..10c08d63612 100644
--- a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
@@ -150,6 +150,7 @@ export default {
:title="$options.i18n.modalTitle"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ no-focus-on-show
@primary="submitImport"
@hidden="resetFields"
>
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 20dc32b3c9b..91b623821dd 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -259,6 +259,7 @@ export default {
dialog-class="gl-mx-5"
:title="modalTitle"
:header-close-label="$options.HEADER_CLOSE_LABEL"
+ no-focus-on-show
@shown="onShowModal"
@close="onClose"
@hidden="onReset"
diff --git a/app/assets/javascripts/profile/components/user_achievements.vue b/app/assets/javascripts/profile/components/user_achievements.vue
index 790b0e9f303..fd42b64f4c5 100644
--- a/app/assets/javascripts/profile/components/user_achievements.vue
+++ b/app/assets/javascripts/profile/components/user_achievements.vue
@@ -29,31 +29,29 @@ export default {
},
methods: {
processNodes(nodes) {
- return nodes.slice(0, 3).map(
- ({
- achievement,
- createdAt,
- achievement: {
- namespace: { fullPath },
+ return nodes.slice(0, 3).map(({ achievement, createdAt, achievement: { namespace } }) => {
+ return {
+ id: `user-achievement-${getIdFromGraphQLId(achievement.id)}`,
+ name: achievement.name,
+ timeAgo: this.timeFormatted(createdAt),
+ avatarUrl: achievement.avatarUrl || gon.gitlab_logo,
+ description: achievement.description,
+ namespace: namespace && {
+ fullPath: namespace.fullPath,
+ webUrl: this.rootUrl + namespace.fullPath,
},
- }) => {
- return {
- id: `user-achievement-${getIdFromGraphQLId(achievement.id)}`,
- name: achievement.name,
- timeAgo: this.timeFormatted(createdAt),
- avatarUrl: achievement.avatarUrl || gon.gitlab_logo,
- description: achievement.description,
- namespace: {
- fullPath,
- webUrl: this.rootUrl + fullPath,
- },
- };
- },
- );
+ };
+ });
+ },
+ achievementAwardedMessage(userAchievement) {
+ return userAchievement.namespace
+ ? this.$options.i18n.awardedBy
+ : this.$options.i18n.awardedByUnknownNamespace;
},
},
i18n: {
awardedBy: s__('Achievements|Awarded %{timeAgo} by %{namespace}'),
+ awardedByUnknownNamespace: s__('Achievements|Awarded %{timeAgo} by a private namespace'),
},
};
</script>
@@ -76,11 +74,11 @@ export default {
<gl-popover triggers="hover focus" placement="top" :target="userAchievement.id">
<div class="gl-font-weight-bold">{{ userAchievement.name }}</div>
<div>
- <gl-sprintf :message="$options.i18n.awardedBy">
+ <gl-sprintf :message="achievementAwardedMessage(userAchievement)">
<template #timeAgo>
<span>{{ userAchievement.timeAgo }}</span>
</template>
- <template #namespace>
+ <template v-if="userAchievement.namespace" #namespace>
<a :href="userAchievement.namespace.webUrl">{{
userAchievement.namespace.fullPath
}}</a>
diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb
index 71f51b9b741..ff4c49dac5a 100644
--- a/app/graphql/types/achievements/achievement_type.rb
+++ b/app/graphql/types/achievements/achievement_type.rb
@@ -14,7 +14,6 @@ module Types
field :namespace,
::Types::NamespaceType,
- null: false,
description: 'Namespace of the achievement.'
field :name,
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
index b9d8735494a..c627f4633e4 100644
--- a/app/mailers/emails/service_desk.rb
+++ b/app/mailers/emails/service_desk.rb
@@ -20,15 +20,24 @@ module Emails
sender_name: @service_desk_setting&.outgoing_name,
sender_email: service_desk_sender_email_address
)
- options = service_desk_options(email_sender, 'thank_you', @issue.external_author)
- .merge(subject: "Re: #{subject_base}")
- inject_service_desk_custom_email(mail_new_thread(@issue, options))
+ options = {
+ from: email_sender,
+ to: @issue.external_author,
+ subject: "Re: #{subject_base}",
+ **service_desk_template_content_options('thank_you')
+ }
+
+ mail_new_thread(@issue, options)
+ inject_service_desk_custom_email
end
def service_desk_new_note_email(issue_id, note_id, recipient)
@note = Note.find(note_id)
+
setup_service_desk_mail(issue_id)
+ # Prepare uploads for text replacement in markdown content
+ setup_service_desk_attachments
email_sender = sender(
@note.author_id,
@@ -36,11 +45,18 @@ module Emails
sender_email: service_desk_sender_email_address
)
- add_uploads_as_attachments if Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project)
- options = service_desk_options(email_sender, 'new_note', recipient)
- .merge(subject: subject_base)
+ options = {
+ from: email_sender,
+ to: recipient,
+ subject: subject_base,
+ **service_desk_template_content_options('new_note')
+ }
- inject_service_desk_custom_email(mail_answer_thread(@issue, options))
+ mail_answer_thread(@issue, options)
+ # Add attachments after email init to guide ActiveMailer
+ # to choose the correct multipart content types
+ add_uploads_as_attachments
+ inject_service_desk_custom_email
end
def service_desk_custom_email_verification_email(service_desk_setting)
@@ -64,13 +80,14 @@ module Emails
from: email_sender,
to: @service_desk_setting.custom_email_address_for_verification,
subject: subject,
- content_type: "text/plain"
+ content_type: "text/plain; charset=UTF-8"
}
# Outgoing emails from GitLab usually have this set to true.
# Service Desk email ingestion ignores auto generated emails.
headers["Auto-Submitted"] = "no"
- inject_service_desk_custom_email(mail_with_locale(options), force: true)
+ mail_with_locale(options)
+ inject_service_desk_custom_email(force: true)
end
def service_desk_verification_triggered_email(service_desk_setting, recipient)
@@ -110,19 +127,16 @@ module Emails
@sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
end
- def service_desk_options(email_sender, email_type, recipient)
- {
- from: email_sender,
- to: recipient
- }.tap do |options|
- next unless template_body = template_content(email_type)
+ def service_desk_template_content_options(email_type)
+ return {} unless template_body = template_content(email_type)
- options[:body] = template_body
- options[:content_type] = 'text/html' unless attachments.present?
- end
+ {
+ body: template_body,
+ content_type: 'text/html; charset=UTF-8'
+ }
end
- def inject_service_desk_custom_email(mail, force: false)
+ def inject_service_desk_custom_email(force: false)
return mail if !service_desk_custom_email_enabled? && !force
return mail unless @service_desk_setting.custom_email_credential.present?
@@ -184,20 +198,32 @@ module Emails
"#{@issue.title} (##{@issue.iid})"
end
- def add_uploads_as_attachments
+ def setup_service_desk_attachments
+ @uploads_to_attach = []
+ # Filepaths we should replace in markdown content
+ @uploads_as_attachments = []
+
+ return unless Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project)
+
uploaders = find_uploaders_for(@note)
- return unless uploaders.present?
+ return if uploaders.nil?
return if uploaders.sum(&:size) > EMAIL_ATTACHMENTS_SIZE_LIMIT
- @uploads_as_attachments = []
uploaders.each do |uploader|
- attachments[uploader.filename] = uploader.read
+ @uploads_to_attach << { filename: uploader.filename, content: uploader.read }
@uploads_as_attachments << "#{uploader.secret}/#{uploader.filename}"
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: @note.project.id)
end
end
+ def add_uploads_as_attachments
+ # We read the uploads before in setup_service_desk_attachments, so let's just add them
+ @uploads_to_attach.each do |upload|
+ mail.add_file(filename: upload[:filename], content: upload[:content])
+ end
+ end
+
def find_uploaders_for(note)
uploads = FileUploader::MARKDOWN_PATTERN.scan(note.note)
return unless uploads.present?
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index eb7db0fc9b4..edc60a757d2 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -30,6 +30,10 @@ class CommitCollection
User.by_any_email(emails)
end
+ def committer_user_ids
+ committers.pluck(:id)
+ end
+
def without_merge_commits
strong_memoize(:without_merge_commits) do
# `#enrich!` the collection to ensure all commits contain
diff --git a/app/models/label.rb b/app/models/label.rb
index 3d9ea39d860..32b399ac461 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -67,6 +67,10 @@ class Label < ApplicationRecord
.with_preloaded_container
end
+ def self.pluck_titles
+ pluck(:title)
+ end
+
def self.prioritized(project)
joins(:priorities)
.where(label_priorities: { project_id: project })
diff --git a/app/models/packages/npm/metadata_cache.rb b/app/models/packages/npm/metadata_cache.rb
index 2d116f2e9c0..7a7c66d7a45 100644
--- a/app/models/packages/npm/metadata_cache.rb
+++ b/app/models/packages/npm/metadata_cache.rb
@@ -3,12 +3,37 @@
module Packages
module Npm
class MetadataCache < ApplicationRecord
+ include FileStoreMounter
+
belongs_to :project, inverse_of: :npm_metadata_caches
- validates :file, :package_name, :project, :size, presence: true
+ validates :file, :object_storage_key, :package_name, :project, :size, presence: true
validates :package_name, uniqueness: { scope: :project_id }
validates :package_name, format: { with: Gitlab::Regex.package_name_regex }
validates :package_name, format: { with: Gitlab::Regex.npm_package_name_regex }
+
+ mount_file_store_uploader MetadataCacheUploader
+
+ before_validation :set_object_storage_key
+ attr_readonly :object_storage_key
+
+ def self.find_or_build(package_name:, project_id:)
+ find_or_initialize_by(
+ package_name: package_name,
+ project_id: project_id
+ )
+ end
+
+ private
+
+ def set_object_storage_key
+ return unless package_name && project_id
+
+ self.object_storage_key = Gitlab::HashedPath.new(
+ 'packages', 'metadata_caches', 'npm', OpenSSL::Digest::SHA256.hexdigest(package_name),
+ root_hash: project_id
+ ).to_s
+ end
end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index d84ba880e71..24bc19d982a 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -150,7 +150,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_counts
end
- rule { can?(:read_group) & achievements_enabled }.policy do
+ rule { achievements_enabled }.policy do
enable :read_achievement
end
diff --git a/app/services/ml/experiment_tracking/candidate_repository.rb b/app/services/ml/experiment_tracking/candidate_repository.rb
index b6814c3df6b..2399da3e182 100644
--- a/app/services/ml/experiment_tracking/candidate_repository.rb
+++ b/app/services/ml/experiment_tracking/candidate_repository.rb
@@ -63,6 +63,8 @@ module Ml
end
def add_tags(candidate, tag_definitions)
+ return unless tag_definitions.present?
+
handle_gitlab_tags(candidate, tag_definitions)
insert_many(candidate, tag_definitions, ::Ml::CandidateMetadata)
diff --git a/app/services/packages/npm/create_metadata_cache_service.rb b/app/services/packages/npm/create_metadata_cache_service.rb
new file mode 100644
index 00000000000..1cc5f7f34e7
--- /dev/null
+++ b/app/services/packages/npm/create_metadata_cache_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class CreateMetadataCacheService
+ include Gitlab::Utils::StrongMemoize
+ include ExclusiveLeaseGuard
+
+ # used by ExclusiveLeaseGuard
+ DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
+
+ def initialize(project, package_name, packages)
+ @project = project
+ @package_name = package_name
+ @packages = packages
+ end
+
+ def execute
+ try_obtain_lease do
+ Packages::Npm::MetadataCache
+ .find_or_build(package_name: package_name, project_id: project.id)
+ .update!(
+ file: CarrierWaveStringFile.new(metadata_content),
+ size: metadata_content.bytesize
+ )
+ end
+ end
+
+ private
+
+ attr_reader :package_name, :packages, :project
+
+ def metadata_content
+ metadata.payload.to_json
+ end
+ strong_memoize_attr :metadata_content
+
+ def metadata
+ Packages::Npm::GenerateMetadataService.new(package_name, packages).execute
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_key
+ "packages:npm:create_metadata_cache_service:metadata_caches:#{project.id}_#{package_name}"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_timeout
+ DEFAULT_LEASE_TIMEOUT
+ end
+ end
+ end
+end
diff --git a/app/uploaders/packages/npm/metadata_cache_uploader.rb b/app/uploaders/packages/npm/metadata_cache_uploader.rb
new file mode 100644
index 00000000000..75a3a94c0b4
--- /dev/null
+++ b/app/uploaders/packages/npm/metadata_cache_uploader.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class MetadataCacheUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ FILENAME = 'metadata.json'
+
+ storage_location :packages
+
+ alias_method :upload, :model
+
+ def filename
+ FILENAME
+ end
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ raise ObjectNotReadyError, 'Packages::Npm::MetadataCache model not ready' unless model.object_storage_key
+
+ model.object_storage_key
+ end
+ end
+ end
+end
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 23bb7170d87..31c541eebde 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -55,9 +55,12 @@
= render_if_exists 'devise/shared/signup_email_additional_info'
.form-group.gl-mb-5
= f.label :password, class: "label-bold #{'gl-mb-1' if Feature.enabled?(:restyle_login_page, @project)}"
- %input.form-control.gl-form-input.js-password{ data: { resource_name: form_resource_name,
+ %input.form-control.gl-form-input.js-password{ data: { id: "#{form_resource_name}_password",
+ title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length },
minimum_password_length: @minimum_password_length,
- qa_selector: 'new_user_password_field' } }
+ qa_selector: 'new_user_password_field',
+ autocomplete: 'new-password',
+ name: "#{form_resource_name}[password]" } }
%p.gl-field-hint-valid.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
= render_if_exists 'shared/password_requirements_list'
= render_if_exists 'devise/shared/phone_verification', form: f
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index f99485ad1eb..6c8ab5654a0 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -29,7 +29,7 @@
.row
.form-group.col-sm-12
= f.label :role, _('Role'), class: 'label-bold'
- = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' }
+ = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', required: true, data: { qa_selector: 'role_dropdown' }
= render_if_exists "registrations/welcome/jobs_to_be_done", f: f
= render_if_exists "registrations/welcome/setup_for_company", f: f
= render_if_exists "registrations/welcome/joining_project"
diff --git a/config/feature_flags/development/always_perform_delayed_deletion.yml b/config/feature_flags/development/always_perform_delayed_deletion.yml
index 6708b5b9f90..df3d318a395 100644
--- a/config/feature_flags/development/always_perform_delayed_deletion.yml
+++ b/config/feature_flags/development/always_perform_delayed_deletion.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393622
milestone: '15.10'
type: development
group: group::tenant scale
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml b/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
index 57b086f52be..ba40608356c 100644
--- a/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
+++ b/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391922
milestone: '15.10'
type: development
group: group::tenant scale
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/use_traversal_ids_groups_finder.yml b/config/feature_flags/development/use_traversal_ids_groups_finder.yml
index f8a90bef1e6..cb4e140769a 100644
--- a/config/feature_flags/development/use_traversal_ids_groups_finder.yml
+++ b/config/feature_flags/development/use_traversal_ids_groups_finder.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345666
milestone: '14.6'
type: development
group: group::tenant scale
-default_enabled: false
+default_enabled: true
diff --git a/db/migrate/20230403093349_ensure_packages_npm_metadata_caches_is_empty.rb b/db/migrate/20230403093349_ensure_packages_npm_metadata_caches_is_empty.rb
new file mode 100644
index 00000000000..baeb368fda1
--- /dev/null
+++ b/db/migrate/20230403093349_ensure_packages_npm_metadata_caches_is_empty.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class EnsurePackagesNpmMetadataCachesIsEmpty < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ truncate_tables!('packages_npm_metadata_caches')
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20230405071033_add_object_storage_key_to_packages_npm_metadata_caches.rb b/db/migrate/20230405071033_add_object_storage_key_to_packages_npm_metadata_caches.rb
new file mode 100644
index 00000000000..a4c0f01ab2a
--- /dev/null
+++ b/db/migrate/20230405071033_add_object_storage_key_to_packages_npm_metadata_caches.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddObjectStorageKeyToPackagesNpmMetadataCaches < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_packages_npm_metadata_caches_on_object_storage_key'
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20230503191056_add_text_limit_to_packages_npm_metadata_caches_object_storage_key
+ def up
+ unless column_exists?(:packages_npm_metadata_caches, :object_storage_key)
+ # The existing table is empty.
+ # rubocop:disable Rails/NotNullColumn
+ add_column :packages_npm_metadata_caches, :object_storage_key, :text, null: false
+ # rubocop:enable Rails/NotNullColumn
+ end
+
+ add_concurrent_index :packages_npm_metadata_caches, :object_storage_key, unique: true, name: INDEX_NAME
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ def down
+ remove_column :packages_npm_metadata_caches, :object_storage_key
+ end
+end
diff --git a/db/migrate/20230503191056_add_text_limit_to_packages_npm_metadata_caches_object_storage_key.rb b/db/migrate/20230503191056_add_text_limit_to_packages_npm_metadata_caches_object_storage_key.rb
new file mode 100644
index 00000000000..b2759a30809
--- /dev/null
+++ b/db/migrate/20230503191056_add_text_limit_to_packages_npm_metadata_caches_object_storage_key.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToPackagesNpmMetadataCachesObjectStorageKey < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :packages_npm_metadata_caches, :object_storage_key, 255
+ end
+
+ def down
+ remove_text_limit :packages_npm_metadata_caches, :object_storage_key
+ end
+end
diff --git a/db/migrate/20230509072635_drop_unused_sequence_by_recreating_vsa_table.rb b/db/migrate/20230509072635_drop_unused_sequence_by_recreating_vsa_table.rb
new file mode 100644
index 00000000000..d1abc9bbda7
--- /dev/null
+++ b/db/migrate/20230509072635_drop_unused_sequence_by_recreating_vsa_table.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class DropUnusedSequenceByRecreatingVsaTable < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def up
+ # dropping is OK since we re-add the table in the same transaction
+ drop_table :value_stream_dashboard_aggregations, if_exists: true # rubocop: disable Migration/DropTable
+ create_table :value_stream_dashboard_aggregations, id: false do |t|
+ # Note: default: nil will prevent SEQUENCE creation
+ t.references :namespace, primary_key: true, null: false, index: false, foreign_key: { on_delete: :cascade },
+ default: nil
+ t.datetime_with_timezone :last_run_at
+ t.boolean :enabled, null: false, default: true
+
+ t.index [:last_run_at, :namespace_id], where: 'enabled IS TRUE',
+ name: 'index_on_value_stream_dashboard_aggregations_last_run_at_id'
+ end
+ end
+
+ def down
+ # no-op, we don't want to restore the sequence
+ end
+end
diff --git a/db/schema_migrations/20230403093349 b/db/schema_migrations/20230403093349
new file mode 100644
index 00000000000..3dcdbd50bcd
--- /dev/null
+++ b/db/schema_migrations/20230403093349
@@ -0,0 +1 @@
+041dd0c558a77b965a9aa66e80eb30ad6e925779db4f69f3a8cf5f6293d25aa2 \ No newline at end of file
diff --git a/db/schema_migrations/20230405071033 b/db/schema_migrations/20230405071033
new file mode 100644
index 00000000000..1412634cfa2
--- /dev/null
+++ b/db/schema_migrations/20230405071033
@@ -0,0 +1 @@
+b5ea5ce5590dfa2d9e989293f641964b2093eebeb606fe9b7a977441c555e9c1 \ No newline at end of file
diff --git a/db/schema_migrations/20230503191056 b/db/schema_migrations/20230503191056
new file mode 100644
index 00000000000..d274119287c
--- /dev/null
+++ b/db/schema_migrations/20230503191056
@@ -0,0 +1 @@
+0421519d9d454666fd00f3d8e6c2c48e889239a6975a52de7aefe19f48ea994f \ No newline at end of file
diff --git a/db/schema_migrations/20230509072635 b/db/schema_migrations/20230509072635
new file mode 100644
index 00000000000..9eec8162720
--- /dev/null
+++ b/db/schema_migrations/20230509072635
@@ -0,0 +1 @@
+1e5bbc3b9d8c244a78e98c60c1f24c3295738334125b8d2b566d97742aee5a97 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f1c82513419..c2e2ddd0e4d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19634,7 +19634,9 @@ CREATE TABLE packages_npm_metadata_caches (
size integer NOT NULL,
file text NOT NULL,
package_name text NOT NULL,
- CONSTRAINT check_57aa07a4b2 CHECK ((char_length(file) <= 255))
+ object_storage_key character varying(255) NOT NULL,
+ CONSTRAINT check_57aa07a4b2 CHECK ((char_length(file) <= 255)),
+ CONSTRAINT check_f97c15aa60 CHECK ((char_length((object_storage_key)::text) <= 255))
);
CREATE SEQUENCE packages_npm_metadata_caches_id_seq
diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md
index 01de5756036..82675c4ce39 100644
--- a/doc/administration/logs/index.md
+++ b/doc/administration/logs/index.md
@@ -1052,9 +1052,13 @@ For Omnibus GitLab installations, NGINX logs are in:
Below is the default GitLab NGINX access log format:
```plaintext
-$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
+'$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"'
```
+The `$request` and `$http_referer` are
+[filtered](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/support/nginx/gitlab)
+for sensitive query string parameters such as secret tokens.
+
## Pages logs
For Omnibus GitLab installations, Pages logs are in `/var/log/gitlab/gitlab-pages/current`.
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 925d3ae6c9b..3eeef5d4afc 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -38,8 +38,8 @@ GET /projects/:id/issues/:issue_iid/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------ |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
```json
[
@@ -130,7 +130,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>"\
### Get single issue discussion item
-Returns a single discussion item for a specific project issue
+Returns a single discussion item for a specific project issue.
```plaintext
GET /projects/:id/issues/:issue_iid/discussions/:discussion_id
@@ -140,18 +140,18 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
-| `discussion_id` | integer | yes | The ID of a discussion item |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
+| `discussion_id` | integer | yes | The ID of a discussion item. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>"
```
### Create new issue thread
-Creates a new thread to a single project issue. This is similar to creating a note but other comments (replies) can be added to it later.
+Creates a new thread to a single project issue. Similar to creating a note, but other comments (replies) can be added to it later.
```plaintext
POST /projects/:id/issues/:issue_iid/discussions
@@ -161,13 +161,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
-| `body` | string | yes | The content of the thread |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/issues/11/discussions?body=comment"
```
@@ -175,7 +175,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
Adds a new note to the thread. This can also [create a thread from a single comment](../user/discussions/index.md#create-a-thread-by-replying-to-a-standard-comment).
-**WARNING**
+WARNING:
Notes can be added to other items than comments, such as system notes, making them threads.
```plaintext
@@ -186,15 +186,15 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights.
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>/notes?body=comment"
```
@@ -210,14 +210,14 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
+| `body` | string | yes | The content of the note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
+| `note_id` | integer | yes | The ID of a thread note. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>/notes/1108?body=comment"
```
@@ -233,13 +233,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `issue_iid` | integer | yes | The IID of an issue |
-| `discussion_id` | integer | yes | The ID of a discussion |
-| `note_id` | integer | yes | The ID of a discussion note |
+| `discussion_id` | integer | yes | The ID of a discussion. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `issue_iid` | integer | yes | The IID of an issue. |
+| `note_id` | integer | yes | The ID of a discussion note. |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/636"
```
@@ -255,8 +255,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `snippet_id` | integer | yes | The ID of an snippet. |
```json
[
@@ -341,13 +341,13 @@ GET /projects/:id/snippets/:snippet_id/discussions
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions"
```
### Get single snippet discussion item
-Returns a single discussion item for a specific project snippet
+Returns a single discussion item for a specific project snippet.
```plaintext
GET /projects/:id/snippets/:snippet_id/discussions/:discussion_id
@@ -357,19 +357,19 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
-| `discussion_id` | integer | yes | The ID of a discussion item |
+| `discussion_id` | integer | yes | The ID of a discussion item.
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `snippet_id` | integer | yes | The ID of an snippet. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>"
```
### Create new snippet thread
-Creates a new thread to a single project snippet. This is similar to creating
-a note but other comments (replies) can be added to it later.
+Creates a new thread to a single project snippet. Similar to creating
+a note, but other comments (replies) can be added to it later.
```plaintext
POST /projects/:id/snippets/:snippet_id/discussions
@@ -379,10 +379,10 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
-| `body` | string | yes | The content of a discussion |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of a discussion. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `snippet_id` | integer | yes | The ID of an snippet. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
@@ -401,15 +401,15 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `snippet_id` | integer | yes | The ID of an snippet. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>/notes?body=comment"
```
@@ -425,14 +425,14 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
+| `body` | string | yes | The content of the note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `snippet_id` | integer | yes | The ID of an snippet. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>/notes/1108?body=comment"
```
@@ -448,13 +448,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `snippet_id` | integer | yes | The ID of an snippet |
-| `discussion_id` | integer | yes | The ID of a discussion |
-| `note_id` | integer | yes | The ID of a discussion note |
+| `discussion_id` | integer | yes | The ID of a discussion. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a discussion note. |
+| `snippet_id` | integer | yes | The ID of an snippet. |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636"
```
@@ -470,8 +470,8 @@ GET /groups/:id/epics/:epic_id/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------ |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
```json
[
@@ -563,7 +563,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>"\
### Get single epic discussion item
-Returns a single discussion item for a specific group epic
+Returns a single discussion item for a specific group epic.
```plaintext
GET /groups/:id/epics/:epic_id/discussions/:discussion_id
@@ -573,19 +573,19 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
-| `discussion_id` | integer | yes | The ID of a discussion item |
+| `discussion_id` | integer | yes | The ID of a discussion item. |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>"
```
### Create new epic thread
-Creates a new thread to a single group epic. This is similar to creating
-a note but other comments (replies) can be added to it later.
+Creates a new thread to a single group epic. Similar to creating
+a note, but other comments (replies) can be added to it later.
```plaintext
POST /groups/:id/epics/:epic_id/discussions
@@ -595,13 +595,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
-| `body` | string | yes | The content of the thread |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the thread. |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment"
```
@@ -618,15 +618,15 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>/notes?body=comment"
```
@@ -642,14 +642,14 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of note/reply |
+| `body` | string | yes | The content of note or reply. |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>/notes/1108?body=comment"
```
@@ -665,13 +665,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
-| `epic_id` | integer | yes | The ID of an epic |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `epic_id` | integer | yes | The ID of an epic. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636"
```
@@ -687,8 +687,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------ |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
```json
[
@@ -839,13 +839,13 @@ Diff comments also contain position:
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions"
```
### Get single merge request discussion item
-Returns a single discussion item for a specific project merge request
+Returns a single discussion item for a specific project merge request.
```plaintext
GET /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id
@@ -855,12 +855,12 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `discussion_id` | integer | yes | The ID of a discussion item |
+| `discussion_id` | integer | yes | The ID of a discussion item. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>"
```
@@ -868,7 +868,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>"\
> The `commit id` entry was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47130) in GitLab 13.7.
-Creates a new thread to a single project merge request. This is similar to creating
+Creates a new thread to a single project merge request. Similar to creating
a note but other comments (replies) can be added to it later. For other approaches,
see [Post comment to commit](commits.md#post-comment-to-commit) in the Commits API,
and [Create new merge request note](notes.md#create-new-merge-request-note) in the Notes API.
@@ -881,30 +881,30 @@ Parameters for all comments:
| Attribute | Type | Required | Description |
| ---------------------------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `body` | string | yes | The content of the thread |
-| `commit_id` | string | no | SHA referencing commit to start this thread on |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
-| `position` | hash | no | Position when creating a diff note |
-| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
-| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
-| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
-| `position[position_type]` | string | yes | Type of the position reference', allowed values: `text` or `image` |
-| `position[new_path]` | string | yes (if the position type is `text`) | File path after change |
-| `position[new_line]` | integer | no | Line number after change (for `text` diff notes) |
-| `position[old_path]` | string | yes (if the position type is `text`) | File path before change |
-| `position[old_line]` | integer | no | Line number before change (for `text` diff notes) |
-| `position[line_range]` | hash | no | Line range for a multi-line diff note |
-| `position[width]` | integer | no | Width of the image (for `image` diff notes) |
-| `position[height]` | integer | no | Height of the image (for `image` diff notes) |
-| `position[x]` | float | no | X coordinate (for `image` diff notes) |
-| `position[y]` | float | no | Y coordinate (for `image` diff notes) |
+| `body` | string | yes | The content of the thread. |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch. |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request. |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch. |
+| `position[new_path]` | string | yes (if the position type is `text`) | File path after change. |
+| `position[old_path]` | string | yes (if the position type is `text`) | File path before change. |
+| `position[position_type]` | string | yes | Type of the position reference. Allowed values: `text` or `image`. |
+| `commit_id` | string | no | SHA referencing commit to start this thread on. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
+| `position` | hash | no | Position when creating a diff note. |
+| `position[new_line]` | integer | no | For `text` diff notes, the line number after change. |
+| `position[old_line]` | integer | no | For `text` diff notes, the line number before change. |
+| `position[line_range]` | hash | no | Line range for a multi-line diff note. |
+| `position[width]` | integer | no | For `image` diff notes, width of the image. |
+| `position[height]` | integer | no | For `image` diff notes, height of the image. |
+| `position[x]` | float | no | For `image` diff notes, X coordinate. |
+| `position[y]` | float | no | For `image` diff notes, Y coordinate. |
#### Create a new thread on the overview page
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment"
```
@@ -918,18 +918,17 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
use `position[old_line]` and don't include `position[new_line]`.
- To create a thread on an unchanged line, include both `position[new_line]` and
`position[old_line]` for the line. These positions might not be the same if earlier
- changes in the file changed the line number. This is a bug that we plan to fix in
- [GraphQL `createDiffNote` forces clients to compute redundant information (#325161)](https://gitlab.com/gitlab-org/gitlab/-/issues/325161).
-- If you specify incorrect `base`/`head`/`start` `SHA` parameters, you might run
- into the following bug:
- [Merge request comments receive "download" link instead of inline code (#296829)](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
+ changes in the file changed the line number. For the discussion about a fix, see
+ [issue 32516](https://gitlab.com/gitlab-org/gitlab/-/issues/325161).
+- If you specify incorrect `base`, `head`, `start`, or `SHA` parameters, you might run
+ into the bug described in [issue #296829)](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
To create a new thread:
1. [Get the latest merge request version](merge_requests.md#get-merge-request-diff-versions):
```shell
- curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/versions"
```
@@ -972,12 +971,12 @@ Parameters for multiline comments only:
| Attribute | Type | Required | Description |
| ---------------------------------------- | -------------- | -------- | ----------- |
-| `position[line_range][start]` | hash | no | Multiline note starting line |
-| `position[line_range][start][line_code]` | string | yes | [Line code](#line-code) for the start line |
-| `position[line_range][start][type]` | string | yes | Use `new` for lines added by this commit, otherwise `old`. |
-| `position[line_range][end]` | hash | no | Multiline note ending line |
-| `position[line_range][end][line_code]` | string | yes | [Line code](#line-code) for the end line |
+| `position[line_range][end][line_code]` | string | yes | [Line code](#line-code) for the end line. |
| `position[line_range][end][type]` | string | yes | Use `new` for lines added by this commit, otherwise `old`. |
+| `position[line_range][start][line_code]` | string | yes | [Line code](#line-code) for the start line. |
+| `position[line_range][start][type]` | string | yes | Use `new` for lines added by this commit, otherwise `old`. |
+| `position[line_range][end]` | hash | no | Multiline note ending line. |
+| `position[line_range][start]` | hash | no | Multiline note starting line. |
#### Line code
@@ -991,9 +990,9 @@ For example, if a commit (`<COMMIT_ID>`) deletes line 463 in the README, you can
on the deletion by referencing line 463 in the *old* file:
```shell
-curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\
- --form "note=Very clever to remove this unnecessary line!"\
- --form "path=README" --form "line=463" --form "line_type=old"\
+curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]" \
+ --form "note=Very clever to remove this unnecessary line!" \
+ --form "path=README" --form "line=463" --form "line_type=old" \
"https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments"
```
@@ -1001,9 +1000,9 @@ If a commit (`<COMMIT_ID>`) adds line 157 to `hello.rb`, you can comment on the
addition by referencing line 157 in the *new* file:
```shell
-curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\
- --form "note=This is brilliant!" --form "path=hello.rb"\
- --form "line=157" --form "line_type=new"\
+curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]" \
+ --form "note=This is brilliant!" --form "path=hello.rb" \
+ --form "line=157" --form "line_type=new" \
"https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments"
```
@@ -1023,13 +1022,13 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `resolved` | boolean | yes | Resolve/unresolve the discussion |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
+| `resolved` | boolean | yes | Resolve or unresolve the discussion. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>?resolved=true"
```
@@ -1046,15 +1045,15 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the note or reply. |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes?body=comment"
```
@@ -1070,22 +1069,22 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | no | The content of the note/reply (exactly one of `body` or `resolved` must be set |
-| `resolved` | boolean | no | Resolve/unresolve the note (exactly one of `body` or `resolved` must be set |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `body` | string | no | The content of the note or reply. Exactly one of `body` or `resolved` must be set. |
+| `resolved` | boolean | no | Resolve or unresolve the note. Exactly one of `body` or `resolved` must be set. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes/1108?body=comment"
```
Resolving a note:
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes/1108?resolved=true"
```
@@ -1101,13 +1100,13 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a merge request |
-| `discussion_id` | integer | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
+| `discussion_id` | integer | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `merge_request_iid` | integer | yes | The IID of a merge request. |
+| `note_id` | integer | yes | The ID of a thread note. |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/636"
```
@@ -1123,8 +1122,8 @@ GET /projects/:id/repository/commits/:commit_id/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------ |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
```json
[
@@ -1255,7 +1254,7 @@ Diff comments contain also position:
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions"
```
@@ -1271,18 +1270,18 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
-| `discussion_id` | string | yes | The ID of a discussion item |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `discussion_id` | string | yes | The ID of a discussion item. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>"
```
### Create new commit thread
-Creates a new thread to a single project commit. This is similar to creating
+Creates a new thread to a single project commit. Similar to creating
a note but other comments (replies) can be added to it later.
```plaintext
@@ -1293,32 +1292,37 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
-| `body` | string | yes | The content of the thread |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
-| `position` | hash | no | Position when creating a diff note |
-| `position[base_sha]` | string | yes | SHA of the parent commit|
-| `position[start_sha]` | string | yes | SHA of the parent commit |
-| `position[head_sha]` | string | yes | The SHA of this commit (same as `commit_id`) |
-| `position[position_type]` | string | yes | Type of the position reference', allowed values: `text` or `image` |
-| `position[new_path]` | string | no | File path after change |
-| `position[new_line]` | integer | no | Line number after change |
-| `position[old_path]` | string | no | File path before change |
-| `position[old_line]` | integer | no | Line number before change |
-| `position[width]` | integer | no | Width of the image (for `image` diff notes) |
-| `position[height]` | integer | no | Height of the image (for `image` diff notes) |
-| `position[x]` | integer | no | X coordinate (for `image` diff notes) |
-| `position[y]` | integer | no | Y coordinate (for `image` diff notes) |
+| `body` | string | yes | The content of the thread. |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `position[base_sha]` | string | yes | SHA of the parent commit. |
+| `position[head_sha]` | string | yes | The SHA of this commit. Same as `commit_id`. |
+| `position[start_sha]` | string | yes | SHA of the parent commit. |
+| `position[position_type]` | string | yes | Type of the position reference. Allowed values: `text` or `image`. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
+| `position` | hash | no | Position when creating a diff note. |
+
+| `position[new_path]` | string | no | File path after change. |
+| `position[new_line]` | integer | no | Line number after change. |
+| `position[old_path]` | string | no | File path before change. |
+| `position[old_line]` | integer | no | Line number before change. |
+| `position[height]` | integer | no | For `image` diff notes, image height. |
+| `position[width]` | integer | no | For `image` diff notes, image width. |
+| `position[x]` | integer | no | For `image` diff notes, X coordinate. |
+| `position[y]` | integer | no | For `image` diff notes, Y coordinate. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions?body=comment"
```
The rules for creating the API request are the same as when
-[creating a new thread in the merge request diff](#create-a-new-thread-in-the-merge-request-diff),
-with the exception of `base_sha`, `start_sha`, and `head_sha` attributes.
+[creating a new thread in the merge request diff](#create-a-new-thread-in-the-merge-request-diff).
+The exceptions:
+
+- `base_sha`
+- `head_sha`
+- `start_sha`
### Add note to existing commit thread
@@ -1332,15 +1336,15 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
-| `discussion_id` | string | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | yes | The content of the note/reply |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, such `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `body` | string | yes | The content of the note or reply. |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `discussion_id` | string | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, such `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes?body=comment
```
@@ -1356,21 +1360,21 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
-| `discussion_id` | string | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
-| `body` | string | no | The content of a note |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `discussion_id` | string | yes | The ID of a thread. |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `note_id` | integer | yes | The ID of a thread note. |
+| `body` | string | no | The content of a note. |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/1108?body=comment"
```
Resolving a note:
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/1108?resolved=true"
```
@@ -1386,12 +1390,12 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) |
-| `commit_id` | string | yes | The SHA of a commit |
-| `discussion_id` | string | yes | The ID of a thread |
-| `note_id` | integer | yes | The ID of a thread note |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
+| `commit_id` | string | yes | The SHA of a commit. |
+| `discussion_id` | string | yes | The ID of a thread. |
+| `note_id` | integer | yes | The ID of a thread note. |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/5/repository/commits/<commit_id>/discussions/<discussion_id>/notes/636"
```
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 8ba5eacebdf..d37ee757c6b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11334,7 +11334,7 @@ Representation of a GitLab user.
| <a id="achievementdescription"></a>`description` | [`String`](#string) | Description or notes for the achievement. |
| <a id="achievementid"></a>`id` | [`AchievementsAchievementID!`](#achievementsachievementid) | ID of the achievement. |
| <a id="achievementname"></a>`name` | [`String!`](#string) | Name of the achievement. |
-| <a id="achievementnamespace"></a>`namespace` | [`Namespace!`](#namespace) | Namespace of the achievement. |
+| <a id="achievementnamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace of the achievement. |
| <a id="achievementupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp the achievement was last updated. |
| <a id="achievementuserachievements"></a>`userAchievements` **{warning-solid}** | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in 15.10. This feature is an Experiment. It can be changed or removed at any time. Recipients for the achievement. |
diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md
index 5107a62f734..f0d6e795a7c 100644
--- a/doc/ci/services/index.md
+++ b/doc/ci/services/index.md
@@ -26,7 +26,7 @@ It's easier and faster to use an existing image and run it as an additional cont
than to install `mysql`, for example, every time the project is built.
You're not limited to only database services. You can add as many
-services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
+services you need to `.gitlab-ci.yml` or manually modify the [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
Any image found at [Docker Hub](https://hub.docker.com/) or your private Container Registry can be
used as a service.
diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md
index c4e752551de..78c9e98c33f 100644
--- a/doc/ci/yaml/artifacts_reports.md
+++ b/doc/ci/yaml/artifacts_reports.md
@@ -142,9 +142,10 @@ following the [CycloneDX](https://cyclonedx.org/docs/1.4) protocol format.
You can specify multiple CycloneDX reports per job. These can be either supplied
as a list of filenames, a filename pattern, or both:
-- List of filenames: `cyclonedx: [gl-sbom-npm-npm.cdx.json, gl-sbom-bundler-gem.cdx.json]`.
-- A filename pattern: `cyclonedx: gl-sbom-*.json`.
-- Combination of both of the above: `cyclonedx: [gl-sbom-*.json, my-cyclonedx.json]`.
+- A filename pattern (`cyclonedx: gl-sbom-*.json`, `junit: test-results/**/*.json`).
+- An array of filenames (`cyclonedx: [gl-sbom-npm-npm.cdx.json, gl-sbom-bundler-gem.cdx.json]`).
+- A combination of both (`cyclonedx: [gl-sbom-*.json, my-cyclonedx.json]`).
+- Directories are not supported(`cyclonedx: test-results`, `cyclonedx: test-results/**`).
Below is an example of a job exposing CycloneDX artifacts:
@@ -239,9 +240,10 @@ GitLab can display the results of one or more reports in:
Some JUnit tools export to multiple XML files. You can specify multiple test report paths in a single job to
concatenate them into a single file. Use either:
-- A filename pattern (`junit: rspec-*.xml`).
-- an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`).
-- A Combination of both (`junit: [rspec.xml, test-results/TEST-*.xml]`).
+- A filename pattern (`junit: rspec-*.xml`, `junit: test-results/**/*.xml`).
+- An array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`).
+- A combination of both (`junit: [rspec.xml, test-results/TEST-*.xml]`).
+- Directories are not supported(`junit: test-results`, `junit: test-results/**`).
## `artifacts:reports:license_scanning` **(ULTIMATE)**
diff --git a/doc/development/sec/token_revocation_api.md b/doc/development/sec/token_revocation_api.md
index 15d1d2d0ef3..e2006ba519c 100644
--- a/doc/development/sec/token_revocation_api.md
+++ b/doc/development/sec/token_revocation_api.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
The Token Revocation API is an externally-deployed HTTP API that interfaces with GitLab
to receive and revoke API tokens and other secrets detected by GitLab Secret Detection.
-See the [high-level architecture](../../user/application_security/secret_detection/post_processing.md)
+See the [high-level architecture](../../user/application_security/secret_detection/automatic_response.md)
to understand the Secret Detection post-processing and revocation flow.
GitLab.com uses the internally-maintained [Secret Revocation Service](https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/secret-revocation-service)
@@ -114,5 +114,5 @@ For example, to configure these values in the
```
After you configure these values, the Token Revocation API will be called according to the
-[high-level architecture](../../user/application_security/secret_detection/post_processing.md#high-level-architecture)
+[high-level architecture](../../user/application_security/secret_detection/automatic_response.md#high-level-architecture)
diagram.
diff --git a/doc/install/docker.md b/doc/install/docker.md
index 215ac32778e..e9cc8098f0b 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -458,9 +458,10 @@ A `docker-compose.yml` example that uses different ports can be found in the
### Configure multiple database connections
-In GitLab 16.0, GitLab will default to using two database connections that point to the same PostgreSQL database.
+In [GitLab 16.0](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/6850),
+GitLab defaults to using two database connections that point to the same PostgreSQL database.
-If you want to opt-in for this feature:
+If, for any reason, you wish to switch back to single database connection:
1. Edit `/etc/gitlab/gitlab.rb` inside the container:
@@ -471,7 +472,7 @@ If you want to opt-in for this feature:
1. Add the following line:
```ruby
- gitlab_rails['databases']['ci']['enable'] = true
+ gitlab_rails['databases']['ci']['enable'] = false
```
1. Restart the container:
diff --git a/doc/topics/gitlab_flow.md b/doc/topics/gitlab_flow.md
index d875f841eec..eb298841247 100644
--- a/doc/topics/gitlab_flow.md
+++ b/doc/topics/gitlab_flow.md
@@ -166,7 +166,7 @@ graph TD
```
This flow is clean and straightforward, and many organizations have adopted it with great success.
-Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/git/simple-git-workflow-is-simple), although they rebase feature branches.
+Another way to integrate change from one branch to another is [rebasing](https://git-scm.com/book/en/v2/Git-Branching-Rebasing).
Merging everything into the `main` branch and frequently deploying means you minimize the amount of unreleased code. This approach is in line with lean and continuous delivery best practices.
However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues.
@@ -179,9 +179,9 @@ and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with i
While this workflow used at GitLab, you can choose whichever workflow
suits your organization best.
-With GitLab flow, we offer additional guidance for these questions.
+With GitLab Flow, we offer additional guidance for these questions.
-## Production branch with GitLab flow
+## Production branch with GitLab Flow
GitHub flow assumes you can deploy to production every time you merge a feature branch.
While this is possible in some cases, such as SaaS applications, there are some cases where this is not possible, such as:
@@ -216,7 +216,7 @@ This time is pretty accurate if you automatically deploy your production branch.
If you need a more exact time, you can have your deployment script create a tag on each deployment.
This flow prevents the overhead of releasing, tagging, and merging that happens with Git flow.
-## Environment branches with GitLab flow
+## Environment branches with GitLab Flow
It might be a good idea to have an environment that is automatically updated to the `staging` branch.
Only, in this case, the name of this environment might differ from the branch name.
@@ -255,7 +255,7 @@ In this case, do not delete the feature branch yet.
If `production` passes automatic testing, you then merge the feature branch into the other branches.
If this is not possible because more manual testing is required, you can send merge requests from the feature branch to the downstream branches.
-## Release branches with GitLab flow
+## Release branches with GitLab Flow
You should work with release branches only if you need to release software to
the outside world. In this case, each branch contains a minor version, such as
@@ -288,7 +288,7 @@ Every time you include a bug fix in a release branch, increase the patch version
Some projects also have a stable branch that points to the same commit as the latest released branch.
In this flow, it is not common to have a production branch (or Git flow `main` branch).
-## Merge/pull requests with GitLab flow
+## Merge/pull requests with GitLab Flow
![Merge request with inline comments](img/gitlab_flow_mr_inline_comments.png)
@@ -306,12 +306,17 @@ The merge request serves as a code review tool, and no separate code review tool
If the review reveals shortcomings, anyone can commit and push a fix.
Usually, the person to do this is the creator of the merge request.
The diff in the merge request automatically updates when new commits are pushed to the branch.
+In GitLab Flow, you can configure your pipeline to run every time you commit changes to a branch. This type of pipeline is called a branch pipeline. Alternatively, you can configure your pipeline to run every time you make changes to the source branch for a merge request. This type of pipeline is called a [merge request pipeline](../ci/pipelines/merge_request_pipelines.md). In GitLab Flow, you can also take advantage of our [Review Apps](../ci/review_apps/index.md) capability, which are a collaboration tool that provide an environment to showcase product changes. Review Apps provide an automatic live preview of changes made in a feature branch by spinning up a dynamic environment for your merge requests allowing stakeholders to see your changes without needing to check out your branch and run your changes in a sandbox environment. When your changes are merged, Review Apps clean up the dynamic environment and related resources preventing environment sprawl.
When you are ready to merge your feature branch, assign the merge request to a maintainer for the project.
Also, mention any other people from whom you would like feedback.
After the assigned person feels comfortable with the result, they can merge the branch.
+In GitLab Flow, a [merged results pipeline](../ci/pipelines/merged_results_pipelines.md) runs against the results of the source and target branches merged together.
If the assigned person does not feel comfortable, they can request more changes or close the merge request without merging.
+NOTE:
+In your pipelines, you can include any automated CI tests for unit, security vulnerabilities, code quality, performance, dependency, etc. Information related to the pipeline runs as well as results of tests are all displayed as widgets in the merge request window, so that you can access and visualize all these from a central location.
+
In GitLab, it is common to protect the long-lived branches, such as the `main` branch, so [most developers can't modify them](../user/permissions.md).
So, if you want to merge into a protected branch, assign your merge request to someone with the
Maintainer role.
@@ -326,9 +331,9 @@ When you reopen an issue you need to create a new merge request.
![Remove checkbox for branch in merge requests](img/gitlab_flow_remove_checkbox.png)
-## Issue tracking with GitLab flow
+## Issue tracking with GitLab Flow
-GitLab flow is a way to make the relation between the code and the issue tracker more transparent.
+GitLab Flow is a way to make the relation between the code and the issue tracker more transparent.
Any significant change to the code should start with an issue that describes the goal.
Having a reason for every code change helps to inform the rest of the team and to keep the scope of a feature branch small.
@@ -365,6 +370,8 @@ In this case, it is no problem to reuse the same branch name, because the first
At any time, there is at most one branch for every issue.
It is possible that one feature branch solves more than one issue.
+In GitLab Flow, you can create a merge request from the issue itself. When you do it this way, a feature branch and its related merge request are automatically created and associated to each other and the merge request is automatically related to the issue. In addition, when the merge request is merged the issue is automatically closed for you.
+
## Linking and closing issues from merge requests
Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12."
@@ -372,6 +379,8 @@ GitLab then creates links to the mentioned issues and creates comments in the is
To automatically close linked issues, mention them with the words "fixes" or "closes," for example, "fixes #14" or "closes #67." GitLab closes these issues when the code is merged into the default branch.
+Like mentioned in the previous section, in GitLab Flow, you can create a merge request from the issue itself. When you do it this way, a feature branch and its related merge request are automatically created and associated to each other and the merge request is automatically related to the issue. In addition, when the merge request is merged the issue is automatically closed for you.
+
If you have an issue that spans across multiple repositories, create an issue for each repository and link all issues to a parent issue.
## Squashing commits with rebase
@@ -435,7 +444,7 @@ However, as discussed in [the section about rebasing](#squashing-commits-with-re
Rebasing could create more work, as every time you rebase, you may need to resolve the same conflicts.
Sometimes you can reuse recorded resolutions (`rerere`), but merging is better, because you only have to resolve conflicts once.
-Atlassian has [a more thorough explanation of the tradeoffs between merging and rebasing](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase) on their blog.
+You can read a more thorough explanation of the tradeoffs between merging and rebasing [here](https://git-scm.com/book/en/v2/Git-Branching-Rebasing#:~:text=Final%20commit%20history-,The,-Perils%20of%20Rebasing).
A good way to prevent creating many merge commits is to not frequently merge `main` into the feature branch.
Three reasons to merge in `main`:
@@ -460,7 +469,7 @@ If your feature branches often take more than a day of work, try to split your f
If you need to keep a feature branch open for more than a day, there are a few strategies to keep it up-to-date.
One option is to use continuous integration (CI) to merge in `main` at the start of the day.
Another option is to only merge in from well-defined points in time, for example, a tagged release.
-You could also use [feature toggles](https://martinfowler.com/bliki/FeatureToggle.html) to hide incomplete features so you can still merge back into `main` every day.
+You could also use [feature toggles](https://martinfowler.com/bliki/FeatureToggle.html) or [feature flags](../operations/feature_flags.md) to hide incomplete features so you can still merge back into `main` every day.
NOTE:
Don't confuse automatic branch testing with continuous integration.
@@ -519,9 +528,9 @@ Issue: gitlab.com/gitlab-org/gitlab/-/issues/1
In old workflows, the continuous integration (CI) server commonly ran tests on the `main` branch only.
Developers had to ensure their code did not break the `main` branch.
-When using GitLab flow, developers create their branches from this `main` branch, so it is essential that it never breaks.
+When using GitLab Flow, developers create their branches from this `main` branch, so it is essential that it never breaks.
Therefore, each merge request must be tested before it is accepted.
-CI software like Travis CI and GitLab CI/CD show the build results right in the merge request itself to simplify the process.
+CI software like GitLab CI/CD shows the build results right in the merge request itself to simplify the process.
There is one drawback to testing merge requests: the CI server only tests the feature branch itself, not the merged result.
Ideally, the server could also test the `main` branch after each change.
@@ -530,6 +539,8 @@ Because feature branches should be short-lived, testing just the branch is an ac
If new commits in `main` cause merge conflicts with the feature branch, merge `main` back into the branch to make the CI server re-run the tests.
As said before, if you often have feature branches that last for more than a few days, you should make your issues smaller.
+In GitLab Flow, your can include automated CI tests in your branch or merge request pipelines, which can run when you commit changes to a branch.
+
## Working with feature branches
When creating a feature branch, always branch from an up-to-date `main`.
diff --git a/doc/user/application_security/secret_detection/automatic_response.md b/doc/user/application_security/secret_detection/automatic_response.md
new file mode 100644
index 00000000000..a898a63e33b
--- /dev/null
+++ b/doc/user/application_security/secret_detection/automatic_response.md
@@ -0,0 +1,242 @@
+---
+stage: Secure
+group: Static Analysis
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Automatic response to leaked secrets **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4639) in GitLab 13.6.
+
+GitLab Secret Detection automatically responds when it finds certain types of leaked secrets.
+Automatic responses can:
+
+- Automatically revoke the secret.
+- Notify the partner that issued the secret. The partner can then revoke the secret, notify its owner, or otherwise protect against abuse.
+
+## Supported secret types and actions
+
+GitLab supports automatic response for the following types of secrets:
+
+| Secret type | Action taken | Supported on GitLab.com | Supported in self-managed |
+| ----- | --- | --- | --- |
+| GitLab [Personal access tokens](../../profile/personal_access_tokens.md) | Immediately revoke token, send email to owner | ✅ | ✅ [15.9 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/371658) |
+| Amazon Web Services (AWS) [IAM access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) | Notify AWS | ✅ | ⚙ |
+
+**Component legend**
+
+- ✅ - Available by default
+- âš™ - Requires manual integration using a [Token Revocation API](../../../development/sec/token_revocation_api.md)
+
+## Feature availability
+
+> [Enabled for non-default branches](https://gitlab.com/gitlab-org/gitlab/-/issues/299212) in GitLab 15.11.
+
+Credentials are only post-processed when Secret Detection finds them:
+
+- In public projects, because publicly exposed credentials pose an increased threat. Expansion to private projects is considered in [issue 391379](https://gitlab.com/gitlab-org/gitlab/-/issues/391379).
+- In projects with GitLab Ultimate, for technical reasons. Expansion to all tiers is tracked in [issue 391763](https://gitlab.com/gitlab-org/gitlab/-/issues/391763).
+
+## High-level architecture
+
+This diagram describes how a post-processing hook revokes a secret in the GitLab application:
+
+```mermaid
+sequenceDiagram
+ autonumber
+ GitLab Rails-->+GitLab Rails: gl-secret-detection-report.json
+ GitLab Rails->>+GitLab Sidekiq: StoreScansService
+ GitLab Sidekiq-->+GitLab Sidekiq: ScanSecurityReportSecretsWorker
+ GitLab Sidekiq-->+GitLab Token Revocation API: GET revocable keys types
+ GitLab Token Revocation API-->>-GitLab Sidekiq: OK
+ GitLab Sidekiq->>+GitLab Token Revocation API: POST revoke revocable keys
+ GitLab Token Revocation API-->>-GitLab Sidekiq: ACCEPTED
+ GitLab Token Revocation API-->>+Partner API: revoke revocable keys
+ Partner API-->>+GitLab Token Revocation API: ACCEPTED
+```
+
+1. A pipeline with a Secret Detection job completes, producing a scan report (**1**).
+1. The report is processed (**2**) by a service class, which schedules an asynchronous worker if token revocation is possible.
+1. The asynchronous worker (**3**) communicates with an externally deployed HTTP service
+ (**4** and **5**) to determine which kinds of secrets can be automatically revoked.
+1. The worker sends (**6** and **7**) the list of detected secrets which the GitLab Token Revocation API is able to
+ revoke.
+1. The GitLab Token Revocation API sends (**8** and **9**) each revocable token to their respective vendor's [Partner API](#implement-a-partner-api). See the [GitLab Token Revocation API](../../../development/sec/token_revocation_api.md)
+ documentation for more information.
+
+## Partner program for leaked-credential notifications
+
+GitLab notifies partners when credentials they issue are leaked in public repositories on GitLab.com.
+If you operate a cloud or SaaS product and you're interested in receiving these notifications, learn more in [epic 4944](https://gitlab.com/groups/gitlab-org/-/epics/4944).
+Partners must [implement a Partner API](#implement-a-partner-api), which is called by the GitLab Token Revocation API.
+
+### Implement a Partner API
+
+A Partner API integrates with the GitLab Token Revocation API to receive and respond to leaked token revocation
+requests. The service should be a publicly accessible HTTP API that is idempotent and rate-limited.
+
+Requests to your service can include one or more leaked tokens, and a header with the signature of the request
+body. We strongly recommend that you verify incoming requests using this signature, to prove it's a genuine
+request from GitLab. The diagram below details the necessary steps to receive, verify, and revoke leaked tokens:
+
+```mermaid
+sequenceDiagram
+ autonumber
+ GitLab Token Revocation API-->>+Partner API: Send new leaked credentials
+ Partner API-->>+GitLab Public Keys endpoint: Get active public keys
+ GitLab Public Keys endpoint-->>+Partner API: One or more public keys
+ Partner API-->>+Partner API: Verify request is signed by GitLab
+ Partner API-->>+Partner API: Respond to leaks
+ Partner API-->>+GitLab Token Revocation API: HTTP status
+```
+
+1. The GitLab Token Revocation API sends (**1**) a [revocation request](#revocation-request) to the Partner API. The request
+ includes headers containing a public key identifier and signature of the request body.
+1. The Partner API requests (**2**) a list of [public keys](#public-keys-endpoint) from GitLab. The response (**3**)
+ may include multiple public keys in the event of key rotation and should be filtered with the identifier in the request header.
+1. The Partner API [verifies the signature](#verifying-the-request) against the actual request body, using the public key (**4**).
+1. The Partner API processes the leaked tokens, which may involve automatic revocation (**5**).
+1. The Partner API responds to the GitLab Token Revocation API (**6**) with the appropriate HTTP status code:
+ - A successful response code (HTTP 200 through 299) acknowledges that the partner has received and processed the request.
+ - An error code (HTTP 400 or higher) causes the GitLab Token Revocation API to retry the request.
+
+#### Revocation request
+
+This JSON schema document describes the body of the revocation request:
+
+```json
+{
+ "type": "array",
+ "items": {
+ "description": "A leaked token",
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "The type of token. This is vendor-specific and can be customised to suit your revocation service",
+ "type": "string",
+ "examples": [
+ "my_api_token"
+ ]
+ },
+ "token": {
+ "description": "The substring that was matched by the Secret Detection analyser. In most cases, this is the entire token itself",
+ "type": "string",
+ "examples": [
+ "XXXXXXXXXXXXXXXX"
+ ]
+ },
+ "url": {
+ "description": "The URL to the raw source file hosted on GitLab where the leaked token was detected",
+ "type": "string",
+ "examples": [
+ "https://gitlab.example.com/some-repo/-/raw/abcdefghijklmnop/compromisedfile1.java"
+ ]
+ }
+ }
+ }
+}
+```
+
+Example:
+
+```json
+[{"type": "my_api_token", "token": "XXXXXXXXXXXXXXXX", "url": "https://example.com/some-repo/-/raw/abcdefghijklmnop/compromisedfile1.java"}]
+```
+
+In this example, Secret Detection has determined that an instance of `my_api_token` has been leaked. The
+value of the token is provided to you, in addition to a publicly accessible URL to the raw content of the
+file containing the leaked token.
+
+The request includes two special headers:
+
+| Header | Type | Description |
+|--------|------|-------------|
+| `Gitlab-Public-Key-Identifier` | string | A unique identifier for the key pair used to sign this request. Primarily used to aid in key rotation. |
+| `Gitlab-Public-Key-Signature` | string | A base64-encoded signature of the request body. |
+
+You can use these headers along with the GitLab Public Keys endpoint to verify that the revocation request was genuine.
+
+#### Public Keys endpoint
+
+GitLab maintains a publicly-accessible endpoint for retrieving public keys used to verify revocation
+requests. The endpoint can be provided on request.
+
+This JSON schema document describes the response body of the public keys endpoint:
+
+```json
+{
+ "type": "object",
+ "properties": {
+ "public_keys": {
+ "description": "An array of public keys managed by GitLab used to sign token revocation requests.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "key_identifier": {
+ "description": "A unique identifier for the keypair. Match this against the value of the Gitlab-Public-Key-Identifier header",
+ "type": "string"
+ },
+ "key": {
+ "description": "The value of the public key",
+ "type": "string"
+ },
+ "is_current": {
+ "description": "Whether the key is currently active and signing new requests",
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+Example:
+
+```json
+{
+ "public_keys": [
+ {
+ "key_identifier": "6917d7584f0fa65c8c33df5ab20f54dfb9a6e6ae",
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEN05/VjsBwWTUGYMpijqC5pDtoLEf\nuWz2CVZAZd5zfa/NAlSFgWRDdNRpazTARndB2+dHDtcHIVfzyVPNr2aznw==\n-----END PUBLIC KEY-----\n",
+ "is_current": true
+ }
+ ]
+}
+```
+
+#### Verifying the request
+
+You can check whether a revocation request is genuine by verifying the `Gitlab-Public-Key-Signature` header
+against the request body, using the corresponding public key taken from the API response above. We use
+[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) with SHA256 hashing to
+produce the signature, which is then base64-encoded into the header value.
+
+The Python script below demonstrates how the signature can be verified. It uses the popular
+[pyca/cryptography](https://cryptography.io/en/latest/) module for cryptographic operations:
+
+```python
+import hashlib
+import base64
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.serialization import load_pem_public_key
+from cryptography.hazmat.primitives.asymmetric import ec
+
+public_key = str.encode("") # obtained from the public keys endpoint
+signature_header = "" # obtained from the `Gitlab-Public-Key-Signature` header
+request_body = str.encode(r'') # obtained from the revocation request body
+
+pk = load_pem_public_key(public_key)
+decoded_signature = base64.b64decode(signature_header)
+
+pk.verify(decoded_signature, request_body, ec.ECDSA(hashes.SHA256())) # throws if unsuccessful
+
+print("Signature verified!")
+```
+
+The main steps are:
+
+1. Loading the public key into a format appropriate for the crypto library you're using.
+1. Base64-decoding the `Gitlab-Public-Key-Signature` header value.
+1. Verifying the body against the decoded signature, specifying ECDSA with SHA256 hashing.
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index c5e10d7a2df..5a1dcc840ca 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -30,7 +30,7 @@ With GitLab Ultimate, Secret Detection results are also processed so you can:
- See them in the [merge request widget](../index.md#view-security-scan-information-in-merge-requests), [pipeline security report](../vulnerability_report/pipeline.md), and [Vulnerability Report](../vulnerability_report/index.md).
- Use them in approval workflows.
- Review them in the security dashboard.
-- [Automatically respond](post_processing.md) to leaks in public repositories.
+- [Automatically respond](automatic_response.md) to leaks in public repositories.
## Detected secrets
@@ -50,7 +50,7 @@ To search for other types of secrets in your repositories, you can configure a [
To propose a new detection rule for all users of Secret Detection, create a merge request against the [file containing the default rules](https://gitlab.com/gitlab-org/security-products/analyzers/secrets/-/blob/master/gitleaks.toml).
-If you operate a cloud or SaaS product and you're interested in partnering with GitLab to better protect your users, learn more about our [partner program for leaked credential notifications](post_processing.md#partner-program-for-leaked-credential-notifications).
+If you operate a cloud or SaaS product and you're interested in partnering with GitLab to better protect your users, learn more about our [partner program for leaked credential notifications](automatic_response.md#partner-program-for-leaked-credential-notifications).
## Features per tier
@@ -199,7 +199,7 @@ Pipelines now include a Secret Detection job.
## Responding to a leaked secret
When a secret is detected, you should rotate it immediately. GitLab attempts to
-[automatically revoke](post_processing.md) some types of leaked secrets. For those that are not
+[automatically revoke](automatic_response.md) some types of leaked secrets. For those that are not
automatically revoked, you must do so manually.
[Purging a secret from the repository's history](../../project/repository/reducing_the_repo_size_using_git.md#purge-files-from-repository-history)
diff --git a/doc/user/application_security/secret_detection/post_processing.md b/doc/user/application_security/secret_detection/post_processing.md
index a898a63e33b..3a6cf7f7e37 100644
--- a/doc/user/application_security/secret_detection/post_processing.md
+++ b/doc/user/application_security/secret_detection/post_processing.md
@@ -1,242 +1,11 @@
---
-stage: Secure
-group: Static Analysis
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+redirect_to: 'automatic_response.md'
+remove_date: '2023-08-08'
---
-# Automatic response to leaked secrets **(ULTIMATE)**
+This document was moved to [another location](automatic_response.md).
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4639) in GitLab 13.6.
-
-GitLab Secret Detection automatically responds when it finds certain types of leaked secrets.
-Automatic responses can:
-
-- Automatically revoke the secret.
-- Notify the partner that issued the secret. The partner can then revoke the secret, notify its owner, or otherwise protect against abuse.
-
-## Supported secret types and actions
-
-GitLab supports automatic response for the following types of secrets:
-
-| Secret type | Action taken | Supported on GitLab.com | Supported in self-managed |
-| ----- | --- | --- | --- |
-| GitLab [Personal access tokens](../../profile/personal_access_tokens.md) | Immediately revoke token, send email to owner | ✅ | ✅ [15.9 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/371658) |
-| Amazon Web Services (AWS) [IAM access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) | Notify AWS | ✅ | ⚙ |
-
-**Component legend**
-
-- ✅ - Available by default
-- âš™ - Requires manual integration using a [Token Revocation API](../../../development/sec/token_revocation_api.md)
-
-## Feature availability
-
-> [Enabled for non-default branches](https://gitlab.com/gitlab-org/gitlab/-/issues/299212) in GitLab 15.11.
-
-Credentials are only post-processed when Secret Detection finds them:
-
-- In public projects, because publicly exposed credentials pose an increased threat. Expansion to private projects is considered in [issue 391379](https://gitlab.com/gitlab-org/gitlab/-/issues/391379).
-- In projects with GitLab Ultimate, for technical reasons. Expansion to all tiers is tracked in [issue 391763](https://gitlab.com/gitlab-org/gitlab/-/issues/391763).
-
-## High-level architecture
-
-This diagram describes how a post-processing hook revokes a secret in the GitLab application:
-
-```mermaid
-sequenceDiagram
- autonumber
- GitLab Rails-->+GitLab Rails: gl-secret-detection-report.json
- GitLab Rails->>+GitLab Sidekiq: StoreScansService
- GitLab Sidekiq-->+GitLab Sidekiq: ScanSecurityReportSecretsWorker
- GitLab Sidekiq-->+GitLab Token Revocation API: GET revocable keys types
- GitLab Token Revocation API-->>-GitLab Sidekiq: OK
- GitLab Sidekiq->>+GitLab Token Revocation API: POST revoke revocable keys
- GitLab Token Revocation API-->>-GitLab Sidekiq: ACCEPTED
- GitLab Token Revocation API-->>+Partner API: revoke revocable keys
- Partner API-->>+GitLab Token Revocation API: ACCEPTED
-```
-
-1. A pipeline with a Secret Detection job completes, producing a scan report (**1**).
-1. The report is processed (**2**) by a service class, which schedules an asynchronous worker if token revocation is possible.
-1. The asynchronous worker (**3**) communicates with an externally deployed HTTP service
- (**4** and **5**) to determine which kinds of secrets can be automatically revoked.
-1. The worker sends (**6** and **7**) the list of detected secrets which the GitLab Token Revocation API is able to
- revoke.
-1. The GitLab Token Revocation API sends (**8** and **9**) each revocable token to their respective vendor's [Partner API](#implement-a-partner-api). See the [GitLab Token Revocation API](../../../development/sec/token_revocation_api.md)
- documentation for more information.
-
-## Partner program for leaked-credential notifications
-
-GitLab notifies partners when credentials they issue are leaked in public repositories on GitLab.com.
-If you operate a cloud or SaaS product and you're interested in receiving these notifications, learn more in [epic 4944](https://gitlab.com/groups/gitlab-org/-/epics/4944).
-Partners must [implement a Partner API](#implement-a-partner-api), which is called by the GitLab Token Revocation API.
-
-### Implement a Partner API
-
-A Partner API integrates with the GitLab Token Revocation API to receive and respond to leaked token revocation
-requests. The service should be a publicly accessible HTTP API that is idempotent and rate-limited.
-
-Requests to your service can include one or more leaked tokens, and a header with the signature of the request
-body. We strongly recommend that you verify incoming requests using this signature, to prove it's a genuine
-request from GitLab. The diagram below details the necessary steps to receive, verify, and revoke leaked tokens:
-
-```mermaid
-sequenceDiagram
- autonumber
- GitLab Token Revocation API-->>+Partner API: Send new leaked credentials
- Partner API-->>+GitLab Public Keys endpoint: Get active public keys
- GitLab Public Keys endpoint-->>+Partner API: One or more public keys
- Partner API-->>+Partner API: Verify request is signed by GitLab
- Partner API-->>+Partner API: Respond to leaks
- Partner API-->>+GitLab Token Revocation API: HTTP status
-```
-
-1. The GitLab Token Revocation API sends (**1**) a [revocation request](#revocation-request) to the Partner API. The request
- includes headers containing a public key identifier and signature of the request body.
-1. The Partner API requests (**2**) a list of [public keys](#public-keys-endpoint) from GitLab. The response (**3**)
- may include multiple public keys in the event of key rotation and should be filtered with the identifier in the request header.
-1. The Partner API [verifies the signature](#verifying-the-request) against the actual request body, using the public key (**4**).
-1. The Partner API processes the leaked tokens, which may involve automatic revocation (**5**).
-1. The Partner API responds to the GitLab Token Revocation API (**6**) with the appropriate HTTP status code:
- - A successful response code (HTTP 200 through 299) acknowledges that the partner has received and processed the request.
- - An error code (HTTP 400 or higher) causes the GitLab Token Revocation API to retry the request.
-
-#### Revocation request
-
-This JSON schema document describes the body of the revocation request:
-
-```json
-{
- "type": "array",
- "items": {
- "description": "A leaked token",
- "type": "object",
- "properties": {
- "type": {
- "description": "The type of token. This is vendor-specific and can be customised to suit your revocation service",
- "type": "string",
- "examples": [
- "my_api_token"
- ]
- },
- "token": {
- "description": "The substring that was matched by the Secret Detection analyser. In most cases, this is the entire token itself",
- "type": "string",
- "examples": [
- "XXXXXXXXXXXXXXXX"
- ]
- },
- "url": {
- "description": "The URL to the raw source file hosted on GitLab where the leaked token was detected",
- "type": "string",
- "examples": [
- "https://gitlab.example.com/some-repo/-/raw/abcdefghijklmnop/compromisedfile1.java"
- ]
- }
- }
- }
-}
-```
-
-Example:
-
-```json
-[{"type": "my_api_token", "token": "XXXXXXXXXXXXXXXX", "url": "https://example.com/some-repo/-/raw/abcdefghijklmnop/compromisedfile1.java"}]
-```
-
-In this example, Secret Detection has determined that an instance of `my_api_token` has been leaked. The
-value of the token is provided to you, in addition to a publicly accessible URL to the raw content of the
-file containing the leaked token.
-
-The request includes two special headers:
-
-| Header | Type | Description |
-|--------|------|-------------|
-| `Gitlab-Public-Key-Identifier` | string | A unique identifier for the key pair used to sign this request. Primarily used to aid in key rotation. |
-| `Gitlab-Public-Key-Signature` | string | A base64-encoded signature of the request body. |
-
-You can use these headers along with the GitLab Public Keys endpoint to verify that the revocation request was genuine.
-
-#### Public Keys endpoint
-
-GitLab maintains a publicly-accessible endpoint for retrieving public keys used to verify revocation
-requests. The endpoint can be provided on request.
-
-This JSON schema document describes the response body of the public keys endpoint:
-
-```json
-{
- "type": "object",
- "properties": {
- "public_keys": {
- "description": "An array of public keys managed by GitLab used to sign token revocation requests.",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "key_identifier": {
- "description": "A unique identifier for the keypair. Match this against the value of the Gitlab-Public-Key-Identifier header",
- "type": "string"
- },
- "key": {
- "description": "The value of the public key",
- "type": "string"
- },
- "is_current": {
- "description": "Whether the key is currently active and signing new requests",
- "type": "boolean"
- }
- }
- }
- }
- }
-}
-```
-
-Example:
-
-```json
-{
- "public_keys": [
- {
- "key_identifier": "6917d7584f0fa65c8c33df5ab20f54dfb9a6e6ae",
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEN05/VjsBwWTUGYMpijqC5pDtoLEf\nuWz2CVZAZd5zfa/NAlSFgWRDdNRpazTARndB2+dHDtcHIVfzyVPNr2aznw==\n-----END PUBLIC KEY-----\n",
- "is_current": true
- }
- ]
-}
-```
-
-#### Verifying the request
-
-You can check whether a revocation request is genuine by verifying the `Gitlab-Public-Key-Signature` header
-against the request body, using the corresponding public key taken from the API response above. We use
-[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) with SHA256 hashing to
-produce the signature, which is then base64-encoded into the header value.
-
-The Python script below demonstrates how the signature can be verified. It uses the popular
-[pyca/cryptography](https://cryptography.io/en/latest/) module for cryptographic operations:
-
-```python
-import hashlib
-import base64
-from cryptography.hazmat.primitives import hashes
-from cryptography.hazmat.primitives.serialization import load_pem_public_key
-from cryptography.hazmat.primitives.asymmetric import ec
-
-public_key = str.encode("") # obtained from the public keys endpoint
-signature_header = "" # obtained from the `Gitlab-Public-Key-Signature` header
-request_body = str.encode(r'') # obtained from the revocation request body
-
-pk = load_pem_public_key(public_key)
-decoded_signature = base64.b64decode(signature_header)
-
-pk.verify(decoded_signature, request_body, ec.ECDSA(hashes.SHA256())) # throws if unsuccessful
-
-print("Signature verified!")
-```
-
-The main steps are:
-
-1. Loading the public key into a format appropriate for the crypto library you're using.
-1. Base64-decoding the `Gitlab-Public-Key-Signature` header value.
-1. Verifying the body against the decoded signature, specifying ECDSA with SHA256 hashing.
+<!-- This redirect file can be deleted after 2023-08-08. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/user/packages/gradle_repository/index.md b/doc/user/packages/gradle_repository/index.md
index 4247c13297d..456acc0da59 100644
--- a/doc/user/packages/gradle_repository/index.md
+++ b/doc/user/packages/gradle_repository/index.md
@@ -1,372 +1,11 @@
---
-stage: Package
-group: Package Registry
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+redirect_to: '../maven_repository/index.md'
+remove_date: '2023-08-09'
---
-# Maven packages in the Package Registry **(FREE)**
+This document was moved to [another location](../maven_repository/index.md).
-Publish [Maven](https://maven.apache.org) artifacts in your project's Package Registry using Gradle.
-Then, install the packages whenever you need to use them as a dependency.
-
-For documentation of the specific API endpoints that the Maven package manager
-client uses, see the [Maven API documentation](../../../api/packages/maven.md).
-
-Learn how to build a [Gradle](../workflows/build_packages.md#gradle) package.
-
-## Publish to the GitLab Package Registry
-
-### Tokens
-
-You need a token to publish a package. Different tokens are available depending on what you're trying to
-achieve. For more information, review the [guidance on tokens](../package_registry/index.md#authenticate-with-the-registry).
-
-- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`.
-- If you publish a package via CI/CD pipelines, you must use a CI job token.
-
-Create a token and save it to use later in the process.
-
-## Authenticate to the Package Registry with Gradle
-
-### Authenticate with a personal access token or deploy token in Gradle
-
-In [your `GRADLE_USER_HOME` directory](https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home),
-create a file `gradle.properties` with the following content:
-
-```properties
-gitLabPrivateToken=REPLACE_WITH_YOUR_TOKEN
-```
-
-Your token name depends on which token you use.
-
-| Token type | Token name |
-| --------------------- | --------------- |
-| Personal access token | `Private-Token` |
-| Deploy token | `Deploy-Token` |
-
-Add a `repositories` section to your
-[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
-file:
-
-```groovy
-repositories {
- maven {
- url "https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven"
- name "GitLab"
- credentials(HttpHeaderCredentials) {
- name = 'REPLACE_WITH_TOKEN_NAME'
- value = gitLabPrivateToken
- }
- authentication {
- header(HttpHeaderAuthentication)
- }
- }
-}
-```
-
-Or add it to your `build.gradle.kts` file if you are using Kotlin DSL:
-
-```kotlin
-repositories {
- maven {
- url = uri("https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven")
- name = "GitLab"
- credentials(HttpHeaderCredentials::class) {
- name = "REPLACE_WITH_TOKEN_NAME"
- value = findProperty("gitLabPrivateToken") as String?
- }
- authentication {
- create("header", HttpHeaderAuthentication::class)
- }
- }
-}
-```
-
-### Authenticate with a CI job token in Gradle
-
-To authenticate with a CI job token, add a `repositories` section to your
-[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
-file:
-
-```groovy
-repositories {
- maven {
- url "${CI_API_V4_URL}/groups/<group>/-/packages/maven"
- name "GitLab"
- credentials(HttpHeaderCredentials) {
- name = 'Job-Token'
- value = System.getenv("CI_JOB_TOKEN")
- }
- authentication {
- header(HttpHeaderAuthentication)
- }
- }
-}
-```
-
-Or add it to your `build.gradle.kts` file if you are using Kotlin DSL:
-
-```kotlin
-repositories {
- maven {
- url = uri("$CI_API_V4_URL/groups/<group>/-/packages/maven")
- name = "GitLab"
- credentials(HttpHeaderCredentials::class) {
- name = "Job-Token"
- value = System.getenv("CI_JOB_TOKEN")
- }
- authentication {
- create("header", HttpHeaderAuthentication::class)
- }
- }
-}
-```
-
-### Naming convention
-
-You can use one of three API endpoints to install a Maven package. You must publish a package to a project, but note which endpoint
-you use to install the package. The option you choose determines the settings you add to your `pom.xml` file for publishing.
-
-The three endpoints are:
-
-- **Project-level**: Use when you have a few Maven packages that are not in the same GitLab group.
-- **Group-level**: Use when installing packages from many different projects in the same GitLab group. GitLab does not guarantee the uniqueness of package names in the group. You can have two projects with the same package name and package version. As a result, GitLab serves whichever one is more recent.
-- **Instance-level**: Use when installing many packages from different GitLab groups or in their own namespace.
-
-**Only packages with the same path as the project** are exposed by the instance-level endpoint.
-
-| Project | Package | Instance-level endpoint available |
-| ------------------- | -------------------------------- | --------------------------------- |
-| `foo/bar` | `foo/bar/1.0-SNAPSHOT` | Yes |
-| `gitlab-org/gitlab` | `foo/bar/1.0-SNAPSHOT` | No |
-| `gitlab-org/gitlab` | `gitlab-org/gitlab/1.0-SNAPSHOT` | Yes |
-
-#### Endpoint URLs
-
-| Endpoint | Endpoint URL | Additional information |
-| -------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
-| Project | `https://gitlab.example.com/api/v4/projects/<project_id>/packages/maven` | Replace `gitlab.example.com` with your domain name. Replace `<project_id>` with your project ID found on your project's homepage. |
-| Group | `https://gitlab.example.com/api/v4/groups/<group_id>/-/packages/maven` | Replace `gitlab.example.com` with your domain name. Replace `<group_id>` with your group ID found on your group's homepage. |
-| Instance | `https:///gitlab.example.com/api/v4/packages/maven` | Replace `gitlab.example.com` with your domain name. |
-
-In all cases, to publish a package, you need:
-
-- A project-specific URL in the `distributionManagement` section.
-- A `repository` and `distributionManagement` section.
-
-### Edit the Groovy DSL or Kotlin DSL
-
-The Gradle Groovy DSL `repositories` section should look like this:
-
-```groovy
-repositories {
- maven {
- url "<your_endpoint_url>"
- name "GitLab"
- }
-}
-```
-
-In Kotlin DSL:
-
-```kotlin
-repositories {
- maven {
- url = uri("<your_endpoint_url>")
- name = "GitLab"
- }
-}
-```
-
-- Replace `<your_endpoint_url>` with the [endpoint](#endpoint-urls) you chose.
-
-## Publish using Gradle
-
-Your token name depends on which token you use.
-
-| Token type | Token name |
-| --------------------- | --------------- |
-| Personal access token | `Private-Token` |
-| Deploy token | `Deploy-Token` |
-
-To publish a package by using Gradle:
-
-1. Add the Gradle plugin [`maven-publish`](https://docs.gradle.org/current/userguide/publishing_maven.html) to the plugins section:
-
- In Groovy DSL:
-
- ```groovy
- plugins {
- id 'java'
- id 'maven-publish'
- }
- ```
-
- In Kotlin DSL:
-
- ```kotlin
- plugins {
- java
- `maven-publish`
- }
- ```
-
-1. Add a `publishing` section:
-
- In Groovy DSL:
-
- ```groovy
- publishing {
- publications {
- library(MavenPublication) {
- from components.java
- }
- }
- repositories {
- maven {
- url "https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/packages/maven"
- credentials(HttpHeaderCredentials) {
- name = "REPLACE_WITH_TOKEN_NAME"
- value = gitLabPrivateToken // the variable resides in $GRADLE_USER_HOME/gradle.properties
- }
- authentication {
- header(HttpHeaderAuthentication)
- }
- }
- }
- }
- ```
-
- In Kotlin DSL:
-
- ```kotlin
- publishing {
- publications {
- create<MavenPublication>("library") {
- from(components["java"])
- }
- }
- repositories {
- maven {
- url = uri("https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/packages/maven")
- credentials(HttpHeaderCredentials::class) {
- name = "REPLACE_WITH_TOKEN_NAME"
- value =
- findProperty("gitLabPrivateToken") as String? // the variable resides in $GRADLE_USER_HOME/gradle.properties
- }
- authentication {
- create("header", HttpHeaderAuthentication::class)
- }
- }
- }
- }
- ```
-
-1. Replace `PROJECT_ID` with your project ID, which you can find on your project's home page.
-
-1. Run the publish task:
-
- ```shell
- gradle publish
- ```
-
-Go to your project's **Packages and registries** page and view the published packages.
-
-## Install a package
-
-To install a package from the GitLab Package Registry, you must configure
-the [remote and authenticate](#authenticate-to-the-package-registry-with-gradle).
-After configuring the remote and authenticate, you can install a package from a project, group, or namespace.
-
-If multiple packages have the same name and version, when you install
-a package, the most recently-published package is retrieved.
-
-Add a [dependency](https://docs.gradle.org/current/userguide/declaring_dependencies.html) to `build.gradle` in the dependencies section:
-
-```groovy
-dependencies {
- implementation 'com.mycompany.mydepartment:my-project:1.0-SNAPSHOT'
-}
-```
-
-Or to `build.gradle.kts` if you are using Kotlin DSL:
-
-```kotlin
-dependencies {
- implementation("com.mycompany.mydepartment:my-project:1.0-SNAPSHOT")
-}
-```
-
-## Helpful hints
-
-For the complete list of helpful hints, see the [Maven documentation](../maven_repository/index.md#helpful-hints).
-
-### Create Maven packages with GitLab CI/CD by using Gradle
-
-You can create a package each time the `main` branch
-is updated.
-
-1. Authenticate with [a CI job token in Gradle](#authenticate-with-a-ci-job-token-in-gradle).
-
-1. Add a `deploy` job to your `.gitlab-ci.yml` file:
-
- ```yaml
- deploy:
- image: gradle:6.5-jdk11
- script:
- - 'gradle publish'
- only:
- - main
- ```
-
-1. Commit files to your repository.
-
-When the pipeline is successful, the Maven package is created.
-
-### Publishing a package with the same name or version
-
-When you publish a package with the same name and version as an existing package, the new package
-files are added to the existing package. You can still use the UI or API to access and view the
-existing package's older assets.
-
-Consider using the Packages API or the UI to delete older package versions.
-
-### Do not allow duplicate Maven packages
-
-To prevent users from publishing duplicate Maven packages, you can use the [GraphQl API](../../../api/graphql/reference/index.md#packagesettings) or the UI.
-
-In the UI:
-
-1. For your group, go to **Settings > Packages and registries**.
-1. Expand the **Package Registry** section.
-1. Turn on the **Do not allow duplicates** toggle.
-1. Optional. To allow some duplicate packages, in the **Exceptions** box, enter a regex pattern that matches the names and/or versions of packages you want to allow.
-
-Your changes are automatically saved.
-
-### Request forwarding to Maven Central
-
-FLAG:
-By default, this feature is not available for self-managed. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `maven_central_request_forwarding`.
-This feature is not available for SaaS users.
-
-When a Maven package is not found in the Package Registry, the request is forwarded
-to [Maven Central](https://search.maven.org/).
-
-When the feature flag is enabled, administrators can disable this behavior in the
-[Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
-
-There are many ways to configure your Maven project to request packages
-in Maven Central from GitLab. Maven repositories are queried in a
-[specific order](https://maven.apache.org/guides/mini/guide-multiple-repositories.html#repository-order).
-By default, maven-central is usually checked first through the
-[Super POM](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#Super_POM), so
-GitLab needs to be configured to be queried before maven-central.
-
-[Using GitLab as a mirror of the central proxy](../maven_repository/index.md#setting-gitlab-as-a-mirror-for-the-central-proxy) is one
-way to force GitLab to be queried in place of maven-central.
-
-Maven forwarding is restricted to only the project level and
-group level [endpoints](#naming-convention). The instance-level endpoint
-has naming restrictions that prevent it from being used for packages that don't follow that convention and also
-introduces too much security risk for supply-chain style attacks.
+<!-- This redirect file can be deleted after <2023-08-09>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 7a451ca283f..72bd7fd5b09 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -12,7 +12,10 @@ Then, install the packages whenever you need to use them as a dependency.
For documentation of the specific API endpoints that the Maven package manager
client uses, see the [Maven API documentation](../../../api/packages/maven.md).
-Learn how to build a [Maven](../workflows/build_packages.md#maven) package.
+Supported clients:
+
+- `mvn`. Learn how to build a [Maven](../workflows/build_packages.md#maven) package.
+- `gradle`. Learn how to build a [Gradle](../workflows/build_packages.md#gradle) package.
## Publish to the GitLab Package Registry
@@ -24,13 +27,14 @@ Create a token and save it to use later in the process.
Do not use authentication methods other than the methods documented here. Undocumented authentication methods might be removed in the future.
-### Edit the `settings.xml`
+#### Edit the client configuration
-Add the following section to your
-[`settings.xml`](https://maven.apache.org/settings.html) file.
+You must add the authentication details to the configuration file
+for your client.
-NOTE:
-The `<name>` field must be named to match the token you chose.
+::Tabs
+
+:::TabTitle `mvn`
| Token type | Name must be | Token |
| --------------------- | --------------- | ---------------------------------------------------------------------- |
@@ -38,6 +42,12 @@ The `<name>` field must be named to match the token you chose.
| Deploy token | `Deploy-Token` | Paste token as-is, or define an environment variable to hold the token |
| CI Job token | `Job-Token` | `${CI_JOB_TOKEN}` |
+NOTE:
+The `<name>` field must be named to match the token you chose.
+
+Add the following section to your
+[`settings.xml`](https://maven.apache.org/settings.html) file.
+
```xml
<settings>
<servers>
@@ -56,6 +66,66 @@ The `<name>` field must be named to match the token you chose.
</settings>
```
+:::TabTitle `gradle`
+
+| Token type | Name must be | Token |
+| --------------------- | --------------- | ---------------------------------------------------------------------- |
+| Personal access token | `Private-Token` | Paste token as-is, or define an environment variable to hold the token |
+| Deploy token | `Deploy-Token` | Paste token as-is, or define an environment variable to hold the token |
+| CI Job token | `Job-Token` | `System.getenv("CI_JOB_TOKEN")` |
+
+NOTE:
+The `<name>` field must be named to match the token you chose.
+
+In [your `GRADLE_USER_HOME` directory](https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home),
+create a file `gradle.properties` with the following content:
+
+```properties
+gitLabPrivateToken=REPLACE_WITH_YOUR_TOKEN
+```
+
+Add a `repositories` section to your
+[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
+file:
+
+- In Groovy DSL:
+
+ ```groovy
+ repositories {
+ maven {
+ url "https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven"
+ name "GitLab"
+ credentials(HttpHeaderCredentials) {
+ name = 'REPLACE_WITH_NAME'
+ value = gitLabPrivateToken
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+ }
+ ```
+
+- In Kotlin DSL:
+
+ ```kotlin
+ repositories {
+ maven {
+ url = uri("https://gitlab.example.com/api/v4/groups/<group>/-/packages/maven")
+ name = "GitLab"
+ credentials(HttpHeaderCredentials::class) {
+ name = "REPLACE_WITH_NAME"
+ value = findProperty("gitLabPrivateToken") as String?
+ }
+ authentication {
+ create("header", HttpHeaderAuthentication::class)
+ }
+ }
+ }
+ ```
+
+::EndTabs
+
### Naming convention
You can use one of three endpoints to install a Maven package. You must publish a package to a project, but the endpoint you choose determines the settings you add to your `pom.xml` file for publishing.
@@ -89,7 +159,13 @@ For the instance-level endpoint, ensure the relevant section of your `pom.xml` i
| Group | `https://gitlab.example.com/api/v4/groups/<group_id>/-/packages/maven` | Replace `gitlab.example.com` with your domain name. Replace `<group_id>` with your group ID, found on your group's homepage. |
| Instance | `https://gitlab.example.com/api/v4/packages/maven` | Replace `gitlab.example.com` with your domain name. |
-### Edit the `pom.xml` for publishing
+### Edit the configuration file for publishing
+
+You must add publishing details to the configuration file for your client.
+
+::Tabs
+
+:::TabTitle `mvn`
No matter which endpoint you choose, you must have:
@@ -117,16 +193,97 @@ The relevant `repository` section of your `pom.xml` in Maven should look like th
</distributionManagement>
```
-- The `id` is what you [defined in `settings.xml`](#edit-the-settingsxml).
+- The `id` is what you [defined in `settings.xml`](#edit-the-client-configuration).
- The `<your_endpoint_url>` depends on which [endpoint](#endpoint-urls) you choose.
- Replace `gitlab.example.com` with your domain name.
+:::TabTitle `gradle`
+
+To publish a package by using Gradle:
+
+1. Add the Gradle plugin [`maven-publish`](https://docs.gradle.org/current/userguide/publishing_maven.html) to the plugins section:
+
+ - In Groovy DSL:
+
+ ```groovy
+ plugins {
+ id 'java'
+ id 'maven-publish'
+ }
+ ```
+
+ - In Kotlin DSL:
+
+ ```kotlin
+ plugins {
+ java
+ `maven-publish`
+ }
+ ```
+
+1. Add a `publishing` section:
+
+ - In Groovy DSL:
+
+ ```groovy
+ publishing {
+ publications {
+ library(MavenPublication) {
+ from components.java
+ }
+ }
+ repositories {
+ maven {
+ url "https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/packages/maven"
+ credentials(HttpHeaderCredentials) {
+ name = "REPLACE_WITH_TOKEN_NAME"
+ value = gitLabPrivateToken // the variable resides in $GRADLE_USER_HOME/gradle.properties
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+ }
+ }
+ ```
+
+ - In Kotlin DSL:
+
+ ```kotlin
+ publishing {
+ publications {
+ create<MavenPublication>("library") {
+ from(components["java"])
+ }
+ }
+ repositories {
+ maven {
+ url = uri("https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/packages/maven")
+ credentials(HttpHeaderCredentials::class) {
+ name = "REPLACE_WITH_TOKEN_NAME"
+ value =
+ findProperty("gitLabPrivateToken") as String? // the variable resides in $GRADLE_USER_HOME/gradle.properties
+ }
+ authentication {
+ create("header", HttpHeaderAuthentication::class)
+ }
+ }
+ }
+ }
+ ```
+
+::EndTabs
+
## Publish a package
After you have set up the [authentication](#authenticate-to-the-package-registry)
and [chosen an endpoint for publishing](#naming-convention),
publish a Maven package to your project.
+::Tabs
+
+:::TabTitle `mvn`
+
To publish a package by using Maven:
```shell
@@ -147,6 +304,18 @@ The message should also show that the package was published to the correct locat
Uploading to gitlab-maven: https://example.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.jar
```
+:::TabTitle `gradle`
+
+Run the publish task:
+
+```shell
+gradle publish
+```
+
+Go to your project's **Packages and registries** page and view the published packages.
+
+::EndTabs
+
## Install a package
To install a package from the GitLab Package Registry, you must configure
@@ -157,7 +326,9 @@ group, or namespace.
If multiple packages have the same name and version, when you install
a package, the most recently-published package is retrieved.
-### Use Maven with `mvn install`
+::Tabs
+
+:::TabTitle `mvn`
To install a package by using `mvn install`:
@@ -184,9 +355,7 @@ The message should show that the package is downloading from the Package Registr
Downloading from gitlab-maven: http://gitlab.example.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.pom
```
-### Use Maven with `mvn dependency:get`
-
-You can install packages by using the Maven `dependency:get` [command](https://maven.apache.org/plugins/maven-dependency-plugin/get-mojo.html) directly.
+You can also install packages by using the Maven [`dependency:get` command](https://maven.apache.org/plugins/maven-dependency-plugin/get-mojo.html) directly.
1. In your project directory, run:
@@ -195,7 +364,7 @@ You can install packages by using the Maven `dependency:get` [command](https://m
```
- `<gitlab endpoint url>` is the URL of the GitLab [endpoint](#endpoint-urls).
- - `<path to settings.xml>` is the path to the `settings.xml` file that contains the [authentication details](#edit-the-settingsxml).
+ - `<path to settings.xml>` is the path to the `settings.xml` file that contains the [authentication details](#edit-the-client-configuration).
NOTE:
The repository IDs in the command(`gitlab-maven`) and the `settings.xml` file must match.
@@ -206,6 +375,36 @@ The message should show that the package is downloading from the Package Registr
Downloading from gitlab-maven: http://gitlab.example.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.pom
```
+:::TabTitle `gradle`
+
+To install a package by using `gradle`:
+
+1. Add a [dependency](https://docs.gradle.org/current/userguide/declaring_dependencies.html) to `build.gradle` in the dependencies section:
+
+ - In Groovy DSL:
+
+ ```groovy
+ dependencies {
+ implementation 'com.mycompany.mydepartment:my-project:1.0-SNAPSHOT'
+ }
+ ```
+
+ - In Kotlin DSL:
+
+ ```kotlin
+ dependencies {
+ implementation("com.mycompany.mydepartment:my-project:1.0-SNAPSHOT")
+ }
+ ```
+
+1. In your project, run the following:
+
+ ```shell
+ gradle install
+ ```
+
+::EndTabs
+
## Helpful hints
### Publishing a package with the same name or version
@@ -241,22 +440,19 @@ to [Maven Central](https://search.maven.org/).
When the feature flag is enabled, administrators can disable this behavior in the
[Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
-There are many ways to configure your Maven project so that it requests packages
-in Maven Central from GitLab. Maven repositories are queried in a
-[specific order](https://maven.apache.org/guides/mini/guide-multiple-repositories.html#repository-order).
-By default, maven-central is usually checked first through the
-[Super POM](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#Super_POM), so
-GitLab needs to be configured to be queried before maven-central.
-
-[Using GitLab as a mirror of the central proxy](#setting-gitlab-as-a-mirror-for-the-central-proxy) is one
-way to force GitLab to be queried in place of maven-central.
-
Maven forwarding is restricted to only the project level and
group level [endpoints](#naming-convention). The instance level endpoint
has naming restrictions that prevent it from being used for packages that don't follow that convention and also
introduces too much security risk for supply-chain style attacks.
-#### Setting GitLab as a mirror for the central proxy
+#### Additional configuration for `mvn`
+
+When using `mvn`, there are many ways to configure your Maven project so that it requests packages
+in Maven Central from GitLab. Maven repositories are queried in a
+[specific order](https://maven.apache.org/guides/mini/guide-multiple-repositories.html#repository-order).
+By default, Maven Central is usually checked first through the
+[Super POM](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#Super_POM), so
+GitLab needs to be configured to be queried before maven-central.
To ensure all package requests are sent to GitLab instead of Maven Central,
you can override Maven Central as the central repository by adding a `<mirror>`
@@ -293,9 +489,11 @@ section to your `settings.xml`:
After you have configured your repository to use the Package Repository for Maven,
you can configure GitLab CI/CD to build new packages automatically.
-### Create Maven packages with GitLab CI/CD using Maven
+::Tabs
-You can create a new package each time the `main` branch is updated.
+:::TabTitle `mvn`
+
+You can create a new package each time the default branch is updated.
1. Create a `ci_settings.xml` file that serves as Maven's `settings.xml` file.
@@ -351,8 +549,8 @@ You can create a new package each time the `main` branch is updated.
image: maven:3.6-jdk-11
script:
- 'mvn deploy -s ci_settings.xml'
- only:
- - main
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```
1. Push those files to your repository.
@@ -363,6 +561,30 @@ user's home location. In this example:
- The user is `root`, because the job runs in a Docker container.
- Maven uses the configured CI/CD variables.
+:::TabTitle `gradle`
+
+You can create a package each time the default branch
+is updated.
+
+1. Authenticate with [a CI job token in Gradle](#edit-the-client-configuration).
+
+1. Add a `deploy` job to your `.gitlab-ci.yml` file:
+
+ ```yaml
+ deploy:
+ image: gradle:6.5-jdk11
+ script:
+ - 'gradle publish'
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ ```
+
+1. Commit files to your repository.
+
+When the pipeline is successful, the Maven package is created.
+
+::EndTabs
+
### Version validation
The version string is validated by using the following regex.
@@ -403,27 +625,44 @@ that you can use when performing tasks with GitLab CI/CD.
### Supported CLI commands
-The GitLab Maven repository supports the following Maven CLI commands:
+The GitLab Maven repository supports the following CLI commands:
+
+::Tabs
+
+:::TabTitle `mvn`
- `mvn deploy`: Publish your package to the Package Registry.
- `mvn install`: Install packages specified in your Maven project.
- `mvn dependency:get`: Install a specific package.
+:::TabTitle `gradle`
+
+- `gradle publish`: Publish your package to the Package Registry.
+- `gradle install`: Install packages specified in your Gradle project.
+
+::EndTabs
+
## Troubleshooting
-To improve performance, Maven caches files related to a package. If you encounter issues, clear
+To improve performance, clients cache files related to a package. If you encounter issues, clear
the cache with these commands:
+::Tabs
+
+:::TabTitle `mvn`
+
```shell
rm -rf ~/.m2/repository
```
-If you're using Gradle, run this command to clear the cache:
+:::TabTitle `gradle`
```shell
rm -rf ~/.gradle/caches # Or replace ~/.gradle with your custom GRADLE_USER_HOME
```
+::EndTabs
+
### Review network trace logs
If you are having issues with the Maven Repository, you may want to review network trace logs.
diff --git a/doc/user/profile/achievements.md b/doc/user/profile/achievements.md
index c8456a80e69..1313c714dd0 100644
--- a/doc/user/profile/achievements.md
+++ b/doc/user/profile/achievements.md
@@ -21,6 +21,8 @@ An achievement consists of a name, a description, and an avatar.
![Achievements on user profile page](img/user_profile_achievements_v15_11.png)
+Achievements are considered to be owned by the user. They are visible regardless of the visibility setting of the namespace that created the Achievement.
+
This feature is an Experiment.
For more information about planned work, see [epic 9429](https://gitlab.com/groups/gitlab-org/-/epics/9429).
Tell us about your use cases by leaving comments in the epic.
@@ -42,7 +44,6 @@ You can view a user's achievements on their profile page.
Prerequisites:
- The user profile must be public.
-- You must be a member of the namespace awarding the achievement, or the namespace must be public.
To view a user's achievements:
@@ -54,7 +55,7 @@ To view a user's achievements:
- Name of the achievement
- Description of the achievement
- Date when the achievement was awarded to the user
- - Namespace that awarded the achievement
+ - Namespace that awarded the achievement if the user is a member of the namespace or the namespace is public
To retrieve a list of a user's achievements, query the [`user` GraphQL type](../../api/graphql/reference/index.md#user).
diff --git a/lib/api/integrations.rb b/lib/api/integrations.rb
index 4691488c19e..0b1a339a864 100644
--- a/lib/api/integrations.rb
+++ b/lib/api/integrations.rb
@@ -238,3 +238,7 @@ module API
end
end
end
+
+# Added for JiHu
+# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118289#note_1379334692
+API::Integrations.prepend_mod
diff --git a/lib/gitlab/usage_data_counters/known_events/product_analytics.yml b/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
index b61e2f4e5a2..5a791c4b3c2 100644
--- a/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
+++ b/lib/gitlab/usage_data_counters/known_events/product_analytics.yml
@@ -1,2 +1,4 @@
- name: project_created_analytics_dashboard
aggregation: weekly
+- name: project_initialized_product_analytics
+ aggregation: weekly
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 74eb8634d58..19a88296120 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -28,7 +28,7 @@ map $http_upgrade $connection_upgrade_gitlab {
}
## NGINX 'combined' log format with filtered query strings
-log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent";
+log_format gitlab_access '$remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent"';
## Remove private_token from the request URI
# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 23d504736e6..02443703423 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -33,7 +33,7 @@ map $http_upgrade $connection_upgrade_gitlab_ssl {
## NGINX 'combined' log format with filtered query strings
-log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent";
+log_format gitlab_ssl_access '$remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent"';
## Remove private_token from the request URI
# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&feed_token=unfiltered&...
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4d990528160..118c9e08558 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2381,6 +2381,9 @@ msgstr ""
msgid "Achievements|Awarded %{timeAgo} by %{namespace}"
msgstr ""
+msgid "Achievements|Awarded %{timeAgo} by a private namespace"
+msgstr ""
+
msgid "Achievements|View your achievements on your %{link_start}profile%{link_end}."
msgstr ""
diff --git a/qa/qa/flow/user_onboarding.rb b/qa/qa/flow/user_onboarding.rb
index a04c29389f5..5c26b0eb5bc 100644
--- a/qa/qa/flow/user_onboarding.rb
+++ b/qa/qa/flow/user_onboarding.rb
@@ -5,9 +5,9 @@ module QA
module UserOnboarding
extend self
- def onboard_user
+ def onboard_user(wait: Capybara.default_max_wait_time)
Page::Registration::Welcome.perform do |welcome_page|
- if welcome_page.has_get_started_button?
+ if welcome_page.has_get_started_button?(wait: wait)
welcome_page.select_role('Other')
welcome_page.choose_setup_for_just_me_if_available
welcome_page.choose_create_a_new_project_if_available
diff --git a/qa/qa/page/registration/welcome.rb b/qa/qa/page/registration/welcome.rb
index 4150208e7ce..bf7b970f12e 100644
--- a/qa/qa/page/registration/welcome.rb
+++ b/qa/qa/page/registration/welcome.rb
@@ -9,8 +9,8 @@ module QA
element :role_dropdown
end
- def has_get_started_button?
- has_element?(:get_started_button)
+ def has_get_started_button?(wait: Capybara.default_max_wait_time)
+ has_element?(:get_started_button, wait: wait)
end
def select_role(role)
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 940462e3805..eb1b1faf9d4 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -1,6 +1,12 @@
safe-to-evict: &safe-to-evict
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
+# We disabled the upgrade checks, as they were giving too many false positives
+#
+# See https://gitlab.com/gitlab-org/quality/engineering-productivity/review-apps-broken-incidents/-/issues/33
+upgradeCheck:
+ enabled: false
+
global:
appConfig:
enableUsagePing: false
diff --git a/spec/factories/packages/npm/metadata_cache.rb b/spec/factories/packages/npm/metadata_cache.rb
index b06915bcb46..e76ddf3c983 100644
--- a/spec/factories/packages/npm/metadata_cache.rb
+++ b/spec/factories/packages/npm/metadata_cache.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :npm_metadata_cache, class: 'Packages::Npm::MetadataCache' do
project
sequence(:package_name) { |n| "@#{project.root_namespace.path}/package-#{n}" }
- file { 'unnamed' }
- size { 100.kilobytes }
+ file { fixture_file_upload('spec/fixtures/packages/npm/metadata.json') }
+ size { 401.bytes }
end
end
diff --git a/spec/fixtures/packages/npm/metadata.json b/spec/fixtures/packages/npm/metadata.json
new file mode 100644
index 00000000000..d23da45fc26
--- /dev/null
+++ b/spec/fixtures/packages/npm/metadata.json
@@ -0,0 +1,20 @@
+{
+ "name": "@root/npm-test",
+ "dist-tags": {
+ "latest": "1.0.1"
+ },
+ "versions": {
+ "1.0.1": {
+ "name": "@root/npm-test",
+ "version": "1.0.1",
+ "main": "app.js",
+ "dependencies": {
+ "express": "^4.16.4"
+ },
+ "dist": {
+ "shasum": "f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball": "http://localhost/npm/package.tgz"
+ }
+ }
+ }
+}
diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js
index 08d725cd4e4..487d1d69de2 100644
--- a/spec/frontend/__mocks__/file_mock.js
+++ b/spec/frontend/__mocks__/file_mock.js
@@ -1 +1 @@
-export default '';
+export default 'file-mock';
diff --git a/spec/frontend/authentication/password/components/password_input_spec.js b/spec/frontend/authentication/password/components/password_input_spec.js
index 623d986b36e..9960539af10 100644
--- a/spec/frontend/authentication/password/components/password_input_spec.js
+++ b/spec/frontend/authentication/password/components/password_input_spec.js
@@ -5,17 +5,21 @@ import { SHOW_PASSWORD, HIDE_PASSWORD } from '~/authentication/password/constant
describe('PasswordInput', () => {
let wrapper;
+ const propsData = {
+ title: 'This field is required',
+ id: 'new_user_password',
+ minimumPasswordLength: '8',
+ qaSelector: 'new_user_password_field',
+ autocomplete: 'new-password',
+ name: 'new_user',
+ };
const findPasswordInput = () => wrapper.findComponent(GlFormInput);
const findToggleButton = () => wrapper.findComponent(GlButton);
const createComponent = () => {
return shallowMount(PasswordInput, {
- propsData: {
- resourceName: 'new_user',
- minimumPasswordLength: '8',
- qaSelector: 'new_user_password_field',
- },
+ propsData,
});
};
@@ -23,6 +27,15 @@ describe('PasswordInput', () => {
wrapper = createComponent();
});
+ it('sets password input attributes correctly', () => {
+ expect(findPasswordInput().attributes('id')).toBe(propsData.id);
+ expect(findPasswordInput().attributes('autocomplete')).toBe(propsData.autocomplete);
+ expect(findPasswordInput().attributes('name')).toBe(propsData.name);
+ expect(findPasswordInput().attributes('minlength')).toBe(propsData.minimumPasswordLength);
+ expect(findPasswordInput().attributes('data-qa-selector')).toBe(propsData.qaSelector);
+ expect(findPasswordInput().attributes('title')).toBe(propsData.title);
+ });
+
describe('when the show password button is clicked', () => {
beforeEach(() => {
findToggleButton().vm.$emit('click');
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index 9c58090344d..0f158df6c05 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -38,7 +38,7 @@ exports[`Comment templates list item component renders list item 1`] = `
role="img"
>
<use
- href="#ellipsis_v"
+ href="file-mock#ellipsis_v"
/>
</svg>
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
index 6271aa87b9a..0e9d7475bf9 100644
--- a/spec/frontend/fixtures/users.rb
+++ b/spec/frontend/fixtures/users.rb
@@ -12,9 +12,11 @@ RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do
context 'for user achievements' do
let_it_be(:group) { create(:group, :public) }
+ let_it_be(:private_group) { create(:group, :private) }
let_it_be(:achievement1) { create(:achievement, namespace: group) }
let_it_be(:achievement2) { create(:achievement, namespace: group) }
let_it_be(:achievement3) { create(:achievement, namespace: group) }
+ let_it_be(:achievement_from_private_group) { create(:achievement, namespace: private_group) }
let_it_be(:achievement_with_avatar_and_description) do
create(:achievement,
namespace: group,
@@ -51,6 +53,14 @@ RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do
expect_graphql_errors_to_be_empty
end
+ it 'graphql/get_user_achievements_from_private_group.json' do
+ create(:user_achievement, user: user, achievement: achievement_from_private_group)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
it "graphql/get_user_achievements_long_response.json" do
[achievement1, achievement2, achievement3, achievement_with_avatar_and_description].each do |achievement|
create(:user_achievement, user: user, achievement: achievement)
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 6766456d09c..abd849b387e 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -76,7 +76,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#arrow-right"
+ href="file-mock#arrow-right"
/>
</svg>
</td>
@@ -113,7 +113,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -144,7 +144,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#search"
+ href="file-mock#search"
/>
</svg>
@@ -201,7 +201,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#arrow-right"
+ href="file-mock#arrow-right"
/>
</svg>
</td>
@@ -238,7 +238,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -269,7 +269,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#search"
+ href="file-mock#search"
/>
</svg>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 5d390730ef1..b4ea6543446 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -42,7 +42,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -122,7 +122,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
role="img"
>
<use
- href="#copy-to-clipboard"
+ href="file-mock#copy-to-clipboard"
/>
</svg>
diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js
index 5b584eff362..ff6f323621a 100644
--- a/spec/frontend/profile/components/user_achievements_spec.js
+++ b/spec/frontend/profile/components/user_achievements_spec.js
@@ -3,12 +3,13 @@ import VueApollo from 'vue-apollo';
import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
+import getUserAchievementsPrivateGroupResponse from 'test_fixtures/graphql/get_user_achievements_from_private_group.json';
import getUserAchievementsNoAvatarResponse from 'test_fixtures/graphql/get_user_achievements_without_avatar_or_description_response.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import UserAchievements from '~/profile/components/user_achievements.vue';
import getUserAchievements from '~/profile/components//graphql/get_user_achievements.query.graphql';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { mountExtended } from 'helpers/vue_test_utils_helper';
const USER_ID = 123;
@@ -62,6 +63,25 @@ describe('UserAchievements', () => {
expect(wrapper.findAllByTestId('user-achievement').length).toBe(3);
});
+ it('renders correctly if the achievement is from a private namespace', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsPrivateGroupResponse),
+ });
+
+ await waitForPromises();
+
+ const userAchievement =
+ getUserAchievementsPrivateGroupResponse.data.user.userAchievements.nodes[0];
+
+ expect(achievement().text()).toContain(userAchievement.achievement.name);
+ expect(achievement().text()).toContain(
+ `Awarded ${getTimeago().format(
+ userAchievement.createdAt,
+ timeagoLanguageCode,
+ )} by a private namespace`,
+ );
+ });
+
it('renders achievement correctly', async () => {
createComponent();
@@ -69,7 +89,7 @@ describe('UserAchievements', () => {
expect(achievement().text()).toContain(userAchievement1.achievement.name);
expect(achievement().text()).toContain(
- `Awarded ${timeagoMixin.methods.timeFormatted(userAchievement1.createdAt)} by`,
+ `Awarded ${getTimeago().format(userAchievement1.createdAt, timeagoLanguageCode)} by`,
);
expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
expect(achievement().text()).toContain(userAchievement1.achievement.description);
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index 45d34bcdd3f..b93c64efbcb 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -17,7 +17,7 @@ exports[`Expand button on click when short text is provided renders button after
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -47,7 +47,7 @@ exports[`Expand button on click when short text is provided renders button after
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -72,7 +72,7 @@ exports[`Expand button when short text is provided renders button before text 1`
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -102,7 +102,7 @@ exports[`Expand button when short text is provided renders button before text 1`
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
diff --git a/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
index dd011b9d84e..1d4aa1afeaf 100644
--- a/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
+++ b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
@@ -2,7 +2,7 @@
exports[`IssuableBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issuable-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
- <use href=\\"#issue-block\\"></use>
+ <use href=\\"file-mock#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
<ul class=\\"gl-list-style-none gl-p-0 gl-mb-0\\">
diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
index 774ad6e93b1..1ee46daa196 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
@@ -183,39 +183,17 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
expect(access_check_queries.values.sum).to eq(5)
end
end
- end
-
- context 'when a project is missing' do
- let_it_be(:included_project) { create(:project, :small_repo, namespace: project.namespace, creator: user) }
-
- let(:files) do
- [
- Gitlab::Ci::Config::External::File::Project.new(
- { file: 'myfolder/file1.yml', project: included_project.full_path }, context
- ),
- Gitlab::Ci::Config::External::File::Project.new(
- { file: 'myfolder/file2.yml', project: 'invalid-project' }, context
- )
- ]
- end
-
- around do |example|
- create_and_delete_files(included_project, project_files) do
- example.run
- end
- end
-
- it 'returns an array of valid file objects' do
- expect(process.map(&:location)).to contain_exactly(
- 'myfolder/file1.yml', 'myfolder/file2.yml'
- )
- expect(process.all?(&:valid?)).to be_falsey
- end
-
- context 'when the FF ci_batch_project_includes_context is disabled' do
- before do
- stub_feature_flags(ci_batch_project_includes_context: false)
+ context 'when a project is missing' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file1.yml', project: included_project1.full_path }, context
+ ),
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file2.yml', project: 'invalid-project' }, context
+ )
+ ]
end
it 'returns an array of file objects' do
@@ -225,6 +203,20 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
expect(process.all?(&:valid?)).to be_falsey
end
+
+ context 'when the FF ci_batch_project_includes_context is disabled' do
+ before do
+ stub_feature_flags(ci_batch_project_includes_context: false)
+ end
+
+ it 'returns an array of file objects' do
+ expect(process.map(&:location)).to contain_exactly(
+ 'myfolder/file1.yml', 'myfolder/file2.yml'
+ )
+
+ expect(process.all?(&:valid?)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 521cbe469d9..c50d5ce2571 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -26,7 +26,25 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
issue.issue_email_participants.create!(email: email)
end
- shared_examples 'handle template content' do |template_key, attachments_count|
+ shared_examples 'a service desk notification email' do |attachments_count|
+ it 'builds the email correctly' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, include_project: false, reply: reply_in_subject)
+
+ expect(subject.attachments.count).to eq(attachments_count.to_i)
+
+ expect(subject.content_type).to include('multipart/alternative')
+
+ expect(subject.parts[0].body.to_s).to include(expected_text)
+ expect(subject.parts[0].content_type).to include('text/plain')
+
+ expect(subject.parts[1].body.to_s).to include(expected_html)
+ expect(subject.parts[1].content_type).to include('text/html')
+ end
+ end
+ end
+
+ shared_examples 'a service desk notification email with template content' do |template_key, attachments_count|
before do
expect(Gitlab::Template::ServiceDeskTemplate).to receive(:find)
.with(template_key, issue.project)
@@ -36,9 +54,18 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
it 'builds the email correctly' do
aggregate_failures do
is_expected.to have_referable_subject(issue, include_project: false, reply: reply_in_subject)
- is_expected.to have_body_text(expected_body)
+ is_expected.to have_body_text(expected_template_html)
+
expect(subject.attachments.count).to eq(attachments_count.to_i)
- expect(subject.content_type).to include(attachments_count.to_i > 0 ? 'multipart/mixed' : 'text/html')
+
+ if attachments_count.to_i > 0
+ # Envelope for emails with attachments is always multipart/mixed
+ expect(subject.content_type).to include('multipart/mixed')
+ # Template content only renders a html body, so ensure its content type is set accordingly
+ expect(subject.parts.first.content_type).to include('text/html')
+ else
+ expect(subject.content_type).to include('text/html')
+ end
end
end
end
@@ -63,7 +90,8 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/another_file.md" => template_content }) }
it 'uses the default template' do
- is_expected.to have_body_text(default_text)
+ expect(subject.text_part.to_s).to include(expected_text)
+ expect(subject.html_part.to_s).to include(expected_html)
end
end
@@ -71,7 +99,8 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
let(:project) { create(:project, :custom_repo, files: { "other_directory/another_file.md" => template_content }) }
it 'uses the default template' do
- is_expected.to have_body_text(default_text)
+ expect(subject.text_part.to_s).to include(expected_text)
+ expect(subject.html_part.to_s).to include(expected_html)
end
end
@@ -79,7 +108,8 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
let(:project) { create(:project) }
it 'uses the default template' do
- is_expected.to have_body_text(default_text)
+ expect(subject.text_part.to_s).to include(expected_text)
+ expect(subject.html_part.to_s).to include(expected_html)
end
end
end
@@ -117,20 +147,23 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
describe '.service_desk_thank_you_email' do
let_it_be(:reply_in_subject) { true }
- let_it_be(:default_text) do
+ let_it_be(:expected_text) do
"Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can."
end
+ let_it_be(:expected_html) { expected_text }
+
subject { ServiceEmailClass.service_desk_thank_you_email(issue.id) }
+ it_behaves_like 'a service desk notification email'
it_behaves_like 'read template from repository', 'thank_you'
context 'handling template markdown' do
context 'with a simple text' do
let(:template_content) { 'thank you, **your new issue** has been created.' }
- let(:expected_body) { 'thank you, <strong>your new issue</strong> has been created.' }
+ let(:expected_template_html) { 'thank you, <strong>your new issue</strong> has been created.' }
- it_behaves_like 'handle template content', 'thank_you'
+ it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
context 'with an issue id, issue path and unsubscribe url placeholders' do
@@ -139,12 +172,12 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
'[Unsubscribe](%{UNSUBSCRIBE_URL})'
end
- let(:expected_body) do
+ let(:expected_template_html) do
"<p dir=\"auto\">thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" \
"<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
end
- it_behaves_like 'handle template content', 'thank_you'
+ it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
context 'with header and footer placeholders' do
@@ -160,42 +193,44 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **your new issue:** %{ ISSUE_ID}' }
- let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
+ let(:expected_template_html) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
- it_behaves_like 'handle template content', 'thank_you'
+ it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
context 'with unexpected placeholder' do
let(:template_content) { 'thank you, **your new issue:** %{this is issue}' }
- let(:expected_body) { "thank you, <strong>your new issue:</strong> %{this is issue}" }
+ let(:expected_template_html) { "thank you, <strong>your new issue:</strong> %{this is issue}" }
- it_behaves_like 'handle template content', 'thank_you'
+ it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
context 'when issue description placeholder is used' do
let(:template_content) { 'thank you, your new issue has been created. %{ISSUE_DESCRIPTION}' }
- let(:expected_body) { "<p dir=\"auto\">thank you, your new issue has been created. </p>#{issue.description_html}" }
+ let(:expected_template_html) { "<p dir=\"auto\">thank you, your new issue has been created. </p>#{issue.description_html}" }
- it_behaves_like 'handle template content', 'thank_you'
+ it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
end
end
describe '.service_desk_new_note_email' do
let_it_be(:reply_in_subject) { false }
- let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project) }
- let_it_be(:default_text) { note.note }
+ let_it_be(:expected_text) { 'My **note**' }
+ let_it_be(:expected_html) { 'My <strong>note</strong>' }
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: expected_text) }
subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id, email) }
+ it_behaves_like 'a service desk notification email'
it_behaves_like 'read template from repository', 'new_note'
- context 'handling template markdown' do
+ context 'with template' do
context 'with a simple text' do
let(:template_content) { 'thank you, **new note on issue** has been created.' }
- let(:expected_body) { 'thank you, <strong>new note on issue</strong> has been created.' }
+ let(:expected_template_html) { 'thank you, <strong>new note on issue</strong> has been created.' }
- it_behaves_like 'handle template content', 'new_note'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
context 'with an issue id, issue path, note and unsubscribe url placeholders' do
@@ -204,12 +239,12 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
'[Unsubscribe](%{UNSUBSCRIBE_URL})'
end
- let(:expected_body) do
- "<p dir=\"auto\">thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" \
+ let(:expected_template_html) do
+ "<p dir=\"auto\">thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{expected_html}" \
"<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
end
- it_behaves_like 'handle template content', 'new_note'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
context 'with header and footer placeholders' do
@@ -225,124 +260,140 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **new note on issue:** %{ ISSUE_ID}: %{ NOTE_TEXT }' }
- let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{note.note}" }
+ let(:expected_template_html) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{expected_html}" }
- it_behaves_like 'handle template content', 'new_note'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
context 'with unexpected placeholder' do
let(:template_content) { 'thank you, **new note on issue:** %{this is issue}' }
- let(:expected_body) { "thank you, <strong>new note on issue:</strong> %{this is issue}" }
+ let(:expected_template_html) { "thank you, <strong>new note on issue:</strong> %{this is issue}" }
- it_behaves_like 'handle template content', 'new_note'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
- context 'with upload link in the note' do
- let_it_be(:secret) { 'e90decf88d8f96fe9e1389afc2e4a91f' }
- let_it_be(:filename) { 'test.jpg' }
- let_it_be(:path) { "#{secret}/#{filename}" }
- let_it_be(:upload_path) { "/uploads/#{path}" }
- let_it_be(:template_content) { 'some text %{ NOTE_TEXT }' }
- let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{filename}](#{upload_path})") }
- let!(:upload) { create(:upload, :issuable_upload, :with_file, model: note.project, path: path, secret: secret) }
+ context 'with all-user reference in a an external author comment' do
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "Hey @all, just a ping", author: User.support_bot) }
- context 'when total uploads size is more than 10mb' do
- before do
- allow_next_instance_of(FileUploader) do |instance|
- allow(instance).to receive(:size).and_return(10.1.megabytes)
- end
- end
+ let(:template_content) { 'some text %{ NOTE_TEXT }' }
+ let(:expected_template_html) { 'Hey , just a ping' }
- let_it_be(:expected_body) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
+ end
+ end
- it_behaves_like 'handle template content', 'new_note'
+ # handle email without and with template in this context to reduce code duplication
+ context 'with upload link in the note' do
+ let_it_be(:secret) { 'e90decf88d8f96fe9e1389afc2e4a91f' }
+ let_it_be(:filename) { 'test.jpg' }
+ let_it_be(:path) { "#{secret}/#{filename}" }
+ let_it_be(:upload_path) { "/uploads/#{path}" }
+ let_it_be(:template_content) { 'some text %{ NOTE_TEXT }' }
+ let_it_be(:expected_text) { "a new comment with [#{filename}](#{upload_path})" }
+ let_it_be(:expected_html) { "a new comment with <strong>#{filename}</strong>" }
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: expected_text) }
+ let!(:upload) { create(:upload, :issuable_upload, :with_file, model: note.project, path: path, secret: secret) }
+
+ context 'when total uploads size is more than 10mb' do
+ before do
+ allow_next_instance_of(FileUploader) do |instance|
+ allow(instance).to receive(:size).and_return(10.1.megabytes)
+ end
end
- context 'when total uploads size is less or equal 10mb' do
- context 'when it has only one upload' do
- before do
- allow_next_instance_of(FileUploader) do |instance|
- allow(instance).to receive(:size).and_return(10.megabytes)
- end
- end
+ let_it_be(:expected_html) { %Q(a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
+ let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
- context 'when upload name is not changed in markdown' do
- let_it_be(:expected_body) { %Q(some text a new comment with <strong>#{filename}</strong>) }
+ it_behaves_like 'a service desk notification email'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
+ end
- it_behaves_like 'handle template content', 'new_note', 1
+ context 'when total uploads size is less or equal 10mb' do
+ context 'when it has only one upload' do
+ before do
+ allow_next_instance_of(FileUploader) do |instance|
+ allow(instance).to receive(:size).and_return(10.megabytes)
+ allow(instance).to receive(:read).and_return('')
end
+ end
- context 'when upload name is changed in markdown' do
- let_it_be(:upload_name_in_markdown) { 'Custom name' }
- let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{upload_name_in_markdown}](#{upload_path})") }
- let_it_be(:expected_body) { %Q(some text a new comment with <strong>#{upload_name_in_markdown} (#{filename})</strong>) }
+ context 'when upload name is not changed in markdown' do
+ let_it_be(:expected_template_html) { %Q(some text a new comment with <strong>#{filename}</strong>) }
- it_behaves_like 'handle template content', 'new_note', 1
- end
+ it_behaves_like 'a service desk notification email', 1
+ it_behaves_like 'a service desk notification email with template content', 'new_note', 1
end
- context 'when it has more than one upload' do
+ context 'when upload name is changed in markdown' do
+ let_it_be(:upload_name_in_markdown) { 'Custom name' }
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{upload_name_in_markdown}](#{upload_path})") }
+ let_it_be(:expected_text) { %Q(a new comment with [#{upload_name_in_markdown}](#{upload_path})) }
+ let_it_be(:expected_html) { %Q(a new comment with <strong>#{upload_name_in_markdown} (#{filename})</strong>) }
+ let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
+
+ it_behaves_like 'a service desk notification email', 1
+ it_behaves_like 'a service desk notification email with template content', 'new_note', 1
+ end
+ end
+
+ context 'when it has more than one upload' do
+ let_it_be(:secret_1) { '17817c73e368777e6f743392e334fb8a' }
+ let_it_be(:filename_1) { 'test1.jpg' }
+ let_it_be(:path_1) { "#{secret_1}/#{filename_1}" }
+ let_it_be(:upload_path_1) { "/uploads/#{path_1}" }
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{filename}](#{upload_path}) [#{filename_1}](#{upload_path_1})") }
+
+ context 'when all uploads processed correct' do
before do
allow_next_instance_of(FileUploader) do |instance|
allow(instance).to receive(:size).and_return(5.megabytes)
+ allow(instance).to receive(:read).and_return('')
end
end
- let_it_be(:secret_1) { '17817c73e368777e6f743392e334fb8a' }
- let_it_be(:filename_1) { 'test1.jpg' }
- let_it_be(:path_1) { "#{secret_1}/#{filename_1}" }
- let_it_be(:upload_path_1) { "/uploads/#{path_1}" }
- let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "a new comment with [#{filename}](#{upload_path}) [#{filename_1}](#{upload_path_1})") }
+ let_it_be(:upload_1) { create(:upload, :issuable_upload, :with_file, model: note.project, path: path_1, secret: secret_1) }
- context 'when all uploads processed correct' do
- let_it_be(:upload_1) { create(:upload, :issuable_upload, :with_file, model: note.project, path: path_1, secret: secret_1) }
- let_it_be(:expected_body) { %Q(some text a new comment with <strong>#{filename}</strong> <strong>#{filename_1}</strong>) }
+ let_it_be(:expected_html) { %Q(a new comment with <strong>#{filename}</strong> <strong>#{filename_1}</strong>) }
+ let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
- it_behaves_like 'handle template content', 'new_note', 2
- end
+ it_behaves_like 'a service desk notification email', 2
+ it_behaves_like 'a service desk notification email with template content', 'new_note', 2
+ end
- context 'when not all uploads processed correct' do
- let_it_be(:expected_body) { %Q(some text a new comment with <strong>#{filename}</strong> <a href="#{project.web_url}#{upload_path_1}" data-canonical-src="#{upload_path_1}" data-link="true" class="gfm">#{filename_1}</a>) }
+ context 'when not all uploads processed correct' do
+ let_it_be(:expected_html) { %Q(a new comment with <strong>#{filename}</strong> <a href="#{project.web_url}#{upload_path_1}" data-canonical-src="#{upload_path_1}" data-link="true" class="gfm">#{filename_1}</a>) }
+ let_it_be(:expected_template_html) { %Q(some text #{expected_html}) }
- it_behaves_like 'handle template content', 'new_note', 1
- end
+ it_behaves_like 'a service desk notification email', 1
+ it_behaves_like 'a service desk notification email with template content', 'new_note', 1
end
end
+ end
- context 'when UploaderFinder is raising error' do
- before do
- allow_next_instance_of(UploaderFinder) do |instance|
- allow(instance).to receive(:execute).and_raise(StandardError)
- end
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
+ context 'when UploaderFinder is raising error' do
+ before do
+ allow_next_instance_of(UploaderFinder) do |instance|
+ allow(instance).to receive(:execute).and_raise(StandardError)
end
-
- let_it_be(:expected_body) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
-
- it_behaves_like 'handle template content', 'new_note'
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
end
- context 'when FileUploader is raising error' do
- before do
- allow_next_instance_of(FileUploader) do |instance|
- allow(instance).to receive(:read).and_raise(StandardError)
- end
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
- end
+ let_it_be(:expected_template_html) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
- let_it_be(:expected_body) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
-
- it_behaves_like 'handle template content', 'new_note'
- end
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
- context 'with all-user reference in a an external author comment' do
- let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project, note: "Hey @all, just a ping", author: User.support_bot) }
+ context 'when FileUploader is raising error' do
+ before do
+ allow_next_instance_of(FileUploader) do |instance|
+ allow(instance).to receive(:read).and_raise(StandardError)
+ end
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(StandardError, project_id: note.project_id)
+ end
- let(:template_content) { 'some text %{ NOTE_TEXT }' }
- let(:expected_body) { 'Hey , just a ping' }
+ let_it_be(:expected_template_html) { %Q(some text a new comment with <a href="#{project.web_url}#{upload_path}" data-canonical-src="#{upload_path}" data-link="true" class="gfm">#{filename}</a>) }
- it_behaves_like 'handle template content', 'new_note'
+ it_behaves_like 'a service desk notification email with template content', 'new_note'
end
end
end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 706f18a5337..1d2d89573bb 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -45,6 +45,24 @@ RSpec.describe CommitCollection, feature_category: :source_code_management do
end
end
+ describe '#committer_user_ids' do
+ subject(:collection) { described_class.new(project, [commit]) }
+
+ it 'returns an array of committer user IDs' do
+ user = create(:user, email: commit.committer_email)
+
+ expect(collection.committer_user_ids).to contain_exactly(user.id)
+ end
+
+ context 'when there are no committers' do
+ subject(:collection) { described_class.new(project, []) }
+
+ it 'returns an empty array' do
+ expect(collection.committer_user_ids).to be_empty
+ end
+ end
+ end
+
describe '#without_merge_commits' do
it 'returns all commits except merge commits' do
merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index ff7ac0ebd2a..26a1edcbcff 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -257,4 +257,15 @@ RSpec.describe Label do
end
end
end
+
+ describe '.pluck_titles' do
+ subject(:pluck_titles) { described_class.pluck_titles }
+
+ it 'returns the audit event type of the event type filter' do
+ label1 = create(:label, title: "TITLE1")
+ label2 = create(:label, title: "TITLE2")
+
+ expect(pluck_titles).to contain_exactly(label1.title, label2.title)
+ end
+ end
end
diff --git a/spec/models/packages/npm/metadata_cache_spec.rb b/spec/models/packages/npm/metadata_cache_spec.rb
index fdee0bedc5b..5e7a710baf8 100644
--- a/spec/models/packages/npm/metadata_cache_spec.rb
+++ b/spec/models/packages/npm/metadata_cache_spec.rb
@@ -3,7 +3,10 @@
require 'spec_helper'
RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :package_registry do
- let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package_name) { '@root/test' }
+
+ it { is_expected.to be_a FileStoreMounter }
describe 'relationships' do
it { is_expected.to belong_to(:project).inverse_of(:npm_metadata_caches) }
@@ -15,17 +18,133 @@ RSpec.describe Packages::Npm::MetadataCache, type: :model, feature_category: :pa
it { is_expected.to validate_presence_of(:size) }
describe '#package_name' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
it { is_expected.to validate_presence_of(:package_name) }
- it { is_expected.to validate_uniqueness_of(:package_name).scoped_to(:project_id) }
- it { is_expected.to allow_value('my.app-11.07.2018').for(:package_name) }
- it { is_expected.to allow_value('@group-1/package').for(:package_name) }
- it { is_expected.to allow_value('@any-scope/package').for(:package_name) }
- it { is_expected.to allow_value('unscoped-package').for(:package_name) }
- it { is_expected.not_to allow_value('my(dom$$$ain)com.my-app').for(:package_name) }
- it { is_expected.not_to allow_value('@inv@lid-scope/package').for(:package_name) }
- it { is_expected.not_to allow_value('@scope/../../package').for(:package_name) }
- it { is_expected.not_to allow_value('@scope%2e%2e%fpackage').for(:package_name) }
- it { is_expected.not_to allow_value('@scope/sub/package').for(:package_name) }
+
+ describe 'uniqueness' do
+ it 'ensures the package name is unique within a given project' do
+ expect do
+ create(:npm_metadata_cache, package_name: package_name, project: project)
+ end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package name has already been taken')
+ end
+
+ it 'allows duplicate file names in different projects' do
+ expect do
+ create(:npm_metadata_cache, package_name: package_name, project: create(:project))
+ end.not_to raise_error
+ end
+ end
+
+ describe 'format' do
+ it { is_expected.to allow_value('my.app-11.07.2018').for(:package_name) }
+ it { is_expected.to allow_value('@group-1/package').for(:package_name) }
+ it { is_expected.to allow_value('@any-scope/package').for(:package_name) }
+ it { is_expected.to allow_value('unscoped-package').for(:package_name) }
+
+ it { is_expected.not_to allow_value('my(dom$$$ain)com.my-app').for(:package_name) }
+ it { is_expected.not_to allow_value('@inv@lid-scope/package').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope/../../package').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope%2e%2e%fpackage').for(:package_name) }
+ it { is_expected.not_to allow_value('@scope/sub/package').for(:package_name) }
+ end
+ end
+ end
+
+ describe '.find_or_build' do
+ subject { described_class.find_or_build(package_name: package_name, project_id: project.id) }
+
+ context 'when a metadata cache exists' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ it 'finds an existing metadata cache' do
+ expect(subject).to eq(npm_metadata_cache)
+ end
+ end
+
+ context 'when a metadata cache not found' do
+ let(:package_name) { 'not_existing' }
+
+ it 'builds a new instance', :aggregate_failures do
+ expect(subject).not_to be_persisted
+ expect(subject.package_name).to eq(package_name)
+ expect(subject.project_id).to eq(project.id)
+ end
+ end
+ end
+
+ describe 'save callbacks' do
+ describe 'object_storage_key' do
+ let(:object_storage_key) do
+ Gitlab::HashedPath.new(
+ 'packages', 'metadata_caches', 'npm', OpenSSL::Digest::SHA256.hexdigest(package_name),
+ root_hash: project.id
+ )
+ end
+
+ before do
+ allow(Gitlab::HashedPath).to receive(:new).and_return(object_storage_key)
+ end
+
+ context 'when the record is created' do
+ let(:npm_metadata_cache) { build(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ it 'sets object_storage_key' do
+ npm_metadata_cache.save!
+
+ expect(npm_metadata_cache.object_storage_key).to eq(object_storage_key.to_s)
+ end
+
+ context 'when using `update!`' do
+ let(:metadata_content) { {}.to_json }
+
+ it 'sets object_storage_key' do
+ npm_metadata_cache.update!(
+ file: CarrierWaveStringFile.new(metadata_content),
+ size: metadata_content.bytesize
+ )
+
+ expect(npm_metadata_cache.object_storage_key).to eq(object_storage_key.to_s)
+ end
+ end
+ end
+
+ context 'when the record is updated' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project: project) }
+
+ let(:existing_object_storage_key) { npm_metadata_cache.object_storage_key }
+ let(:new_package_name) { 'updated_package_name' }
+
+ it 'does not update object_storage_key' do
+ existing_object_storage_key = npm_metadata_cache.object_storage_key
+
+ npm_metadata_cache.update!(package_name: new_package_name)
+
+ expect(npm_metadata_cache.object_storage_key).to eq(existing_object_storage_key)
+ end
+ end
+ end
+ end
+
+ describe 'readonly attributes' do
+ describe 'object_storage_key' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache) }
+
+ it 'sets object_storage_key' do
+ expect(npm_metadata_cache.object_storage_key).to be_present
+ end
+
+ context 'when the record is persisted' do
+ let(:new_object_storage_key) { 'object/storage/updated_key' }
+
+ it 'does not re-set object_storage_key' do
+ npm_metadata_cache.object_storage_key = new_object_storage_key
+
+ npm_metadata_cache.save!
+
+ expect(npm_metadata_cache.object_storage_key).not_to eq(new_object_storage_key)
+ end
+ end
end
end
end
diff --git a/spec/policies/achievements/user_achievement_policy_spec.rb b/spec/policies/achievements/user_achievement_policy_spec.rb
index 47f6188e178..c3148e882fa 100644
--- a/spec/policies/achievements/user_achievement_policy_spec.rb
+++ b/spec/policies/achievements/user_achievement_policy_spec.rb
@@ -56,8 +56,8 @@ RSpec.describe Achievements::UserAchievementPolicy, feature_category: :user_prof
context 'for achievement owner' do
let(:current_user) { achievement_owner }
- it 'is hidden' do
- is_expected.not_to be_allowed(:read_user_achievement)
+ it 'is visible' do
+ is_expected.to be_allowed(:read_user_achievement)
end
end
@@ -70,8 +70,8 @@ RSpec.describe Achievements::UserAchievementPolicy, feature_category: :user_prof
end
context 'for others' do
- it 'is hidden' do
- is_expected.not_to be_allowed(:read_user_achievement)
+ it 'is visible' do
+ is_expected.to be_allowed(:read_user_achievement)
end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 462848bc832..5e85a6e187b 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1753,5 +1753,11 @@ RSpec.describe GroupPolicy, feature_category: :system_access do
specify { is_expected.to be_disallowed(:admin_achievement) }
specify { is_expected.to be_disallowed(:award_achievement) }
end
+
+ context 'when current user can not see the group' do
+ let(:current_user) { non_group_member }
+
+ specify { is_expected.to be_allowed(:read_achievement) }
+ end
end
end
diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
index bf9b2b429cc..be67009784b 100644
--- a/spec/requests/api/graphql/user/user_achievements_query_spec.rb
+++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
@@ -86,6 +86,11 @@ RSpec.describe 'UserAchievements', feature_category: :user_profile do
context 'when current user is not a member of the private group' do
let(:current_user) { create(:user) }
- specify { expect(graphql_data_at(:user, :userAchievements, :nodes)).to be_empty }
+ it 'returns all achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievements[0]),
+ a_graphql_entity_for(user_achievements[1])
+ )
+ end
end
end
diff --git a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
index eb76d1c5787..079c36c9613 100644
--- a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
+++ b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
@@ -299,5 +299,15 @@ RSpec.describe ::Ml::ExperimentTracking::CandidateRepository, feature_category:
expect { subject }.to change { candidate.reload.metadata.size }.by(1)
end
end
+
+ context 'when tags is nil' do
+ let(:tags) { nil }
+
+ it 'does not handle gitlab tags' do
+ expect(repository).not_to receive(:handle_gitlab_tags)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/packages/npm/create_metadata_cache_service_spec.rb b/spec/services/packages/npm/create_metadata_cache_service_spec.rb
new file mode 100644
index 00000000000..75f822f0ddb
--- /dev/null
+++ b/spec/services/packages/npm/create_metadata_cache_service_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::CreateMetadataCacheService, :clean_gitlab_redis_shared_state, feature_category: :package_registry do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package_name) { "@#{project.root_namespace.path}/npm-test" }
+ let_it_be(:package) { create(:npm_package, version: '1.0.0', project: project, name: package_name) }
+
+ let(:packages) { project.packages }
+ let(:lease_key) { "packages:npm:create_metadata_cache_service:metadata_caches:#{project.id}_#{package_name}" }
+ let(:service) { described_class.new(project, package_name, packages) }
+
+ describe '#execute' do
+ let(:npm_metadata_cache) { Packages::Npm::MetadataCache.last }
+
+ subject { service.execute }
+
+ it 'creates a new metadata cache', :aggregate_failures do
+ expect { subject }.to change { Packages::Npm::MetadataCache.count }.by(1)
+
+ metadata = Gitlab::Json.parse(npm_metadata_cache.file.read)
+
+ expect(npm_metadata_cache.package_name).to eq(package_name)
+ expect(npm_metadata_cache.project_id).to eq(project.id)
+ expect(npm_metadata_cache.size).to eq(metadata.to_json.bytesize)
+ expect(metadata['name']).to eq(package_name)
+ expect(metadata['versions'].keys).to contain_exactly('1.0.0')
+ end
+
+ context 'with existing metadata cache' do
+ let_it_be(:npm_metadata_cache) { create(:npm_metadata_cache, package_name: package_name, project_id: project.id) }
+ let_it_be(:metadata) { Gitlab::Json.parse(npm_metadata_cache.file.read) }
+ let_it_be(:metadata_size) { npm_metadata_cache.size }
+ let_it_be(:tag_name) { 'new-tag' }
+ let_it_be(:tag) { create(:packages_tag, package: package, name: tag_name) }
+
+ it 'does not create a new metadata cache' do
+ expect { subject }.to change { Packages::Npm::MetadataCache.count }.by(0)
+ end
+
+ it 'updates the metadata cache', :aggregate_failures do
+ subject
+
+ new_metadata = Gitlab::Json.parse(npm_metadata_cache.file.read)
+
+ expect(new_metadata).not_to eq(metadata)
+ expect(new_metadata['dist_tags'].keys).to include(tag_name)
+ expect(npm_metadata_cache.reload.size).not_to eq(metadata_size)
+ end
+ end
+
+ it 'obtains a lease to create a new metadata cache' do
+ expect_to_obtain_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+
+ subject
+ end
+
+ context 'when the lease is already taken' do
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+ end
+
+ it 'does not create a new metadata cache' do
+ expect { subject }.to change { Packages::Npm::MetadataCache.count }.by(0)
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ describe '#lease_key' do
+ subject { service.send(:lease_key) }
+
+ it 'returns an unique key' do
+ is_expected.to eq lease_key
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index a9b12d47393..111fd3dc7df 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -12,7 +12,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:public_permissions) do
%i[
- read_group read_counts read_achievement
+ read_group read_counts
read_label read_issue_board_list read_milestone read_issue_board
]
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index 17e48d6b581..7c20ea661b5 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -1,13 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'rejects package tags access' do |status:|
- before do
- package.update!(name: package_name) unless package_name == 'non-existing-package'
- end
-
- it_behaves_like 'returning response status', status
-end
-
RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax
include_context 'dependency proxy helpers context'
diff --git a/spec/uploaders/packages/npm/metadata_cache_uploader_spec.rb b/spec/uploaders/packages/npm/metadata_cache_uploader_spec.rb
new file mode 100644
index 00000000000..0bcf05932a5
--- /dev/null
+++ b/spec/uploaders/packages/npm/metadata_cache_uploader_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::MetadataCacheUploader, feature_category: :package_registry do
+ let(:object_storage_key) { 'object/storage/key' }
+ let(:npm_metadata_cache) { build_stubbed(:npm_metadata_cache, object_storage_key: object_storage_key) }
+
+ subject { described_class.new(npm_metadata_cache, :file) }
+
+ describe '#filename' do
+ it 'returns metadata.json' do
+ expect(subject.filename).to eq('metadata.json')
+ end
+ end
+
+ describe '#store_dir' do
+ it 'uses the object_storage_key' do
+ expect(subject.store_dir).to eq(object_storage_key)
+ end
+
+ context 'without the object_storage_key' do
+ let(:object_storage_key) { nil }
+
+ it 'raises the error' do
+ expect { subject.store_dir }
+ .to raise_error(
+ described_class::ObjectNotReadyError,
+ 'Packages::Npm::MetadataCache model not ready'
+ )
+ end
+ end
+ end
+end