Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
63c5b4906b
commit
b45d30ab76
|
|
@ -213,7 +213,7 @@ export default {
|
|||
class="js-file-title file-title file-title-flex-parent"
|
||||
@click.self="handleToggleFile"
|
||||
>
|
||||
<div class="file-header-content gl-display-flex gl-align-items-center gl-pr-0!">
|
||||
<div class="file-header-content">
|
||||
<gl-icon
|
||||
v-if="collapsible"
|
||||
ref="collapseIcon"
|
||||
|
|
@ -226,7 +226,7 @@ export default {
|
|||
<a
|
||||
ref="titleWrapper"
|
||||
:v-once="!viewDiffsFileByFile"
|
||||
class="gl-mr-2 gl-text-decoration-none! gl-text-truncate"
|
||||
class="gl-mr-2 gl-text-decoration-none! gl-word-break-all"
|
||||
:href="titleLink"
|
||||
@click="handleFileNameClick"
|
||||
>
|
||||
|
|
@ -280,12 +280,12 @@ export default {
|
|||
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
|
||||
</small>
|
||||
|
||||
<span v-if="isUsingLfs" class="label label-lfs gl-mr-2"> {{ __('LFS') }} </span>
|
||||
<span v-if="isUsingLfs" class="badge label label-lfs gl-mr-2"> {{ __('LFS') }} </span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!diffFile.submodule && addMergeRequestButtons"
|
||||
class="file-actions d-flex align-items-center flex-wrap"
|
||||
class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start"
|
||||
>
|
||||
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
|
||||
<gl-button-group class="gl-pt-0!">
|
||||
|
|
|
|||
|
|
@ -45,11 +45,6 @@
|
|||
}
|
||||
|
||||
.file-actions {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 15px;
|
||||
margin-left: auto;
|
||||
|
||||
.btn:not(.btn-icon) {
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
|
|
@ -342,30 +337,14 @@ span.idiff {
|
|||
padding: $gl-padding-8 $gl-padding;
|
||||
margin: 0;
|
||||
border-radius: $border-radius-default $border-radius-default 0 0;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.file-header-content {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 30px;
|
||||
position: relative;
|
||||
width: auto;
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, sm)-1) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.file-holder & {
|
||||
.file-actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-clipboard {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
@ -384,15 +363,11 @@ span.idiff {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
@include media-breakpoint-down(sm) {
|
||||
display: block;
|
||||
|
||||
.file-actions {
|
||||
white-space: normal;
|
||||
|
||||
.btn-group {
|
||||
padding-top: 5px;
|
||||
}
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class SearchController < ApplicationController
|
|||
SCOPE_PRELOAD_METHOD = {
|
||||
projects: :with_web_entity_associations,
|
||||
issues: :with_web_entity_associations,
|
||||
merge_requests: :with_web_entity_associations,
|
||||
epics: :with_web_entity_associations
|
||||
}.freeze
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,13 @@ class GitlabSchema < GraphQL::Schema
|
|||
find_by_gid(gid)
|
||||
end
|
||||
|
||||
def resolve_type(type, object, ctx = :__undefined__)
|
||||
tc = type.metadata[:type_class]
|
||||
return if tc.respond_to?(:assignable?) && !tc.assignable?(object)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
# Find an object by looking it up from its 'GlobalID'.
|
||||
#
|
||||
# * For `ApplicationRecord`s, this is equivalent to
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ module Types
|
|||
|
||||
field_class Types::BaseField
|
||||
|
||||
def self.accepts(*types)
|
||||
@accepts ||= []
|
||||
@accepts += types
|
||||
@accepts
|
||||
end
|
||||
|
||||
# All graphql fields exposing an id, should expose a global id.
|
||||
def id
|
||||
GitlabSchema.id_from_object(object)
|
||||
|
|
@ -16,5 +22,13 @@ module Types
|
|||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def self.assignable?(object)
|
||||
assignable = accepts
|
||||
|
||||
return true if assignable.blank?
|
||||
|
||||
assignable.any? { |cls| object.is_a?(cls) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Types
|
|||
class BoardType < BaseObject
|
||||
graphql_name 'Board'
|
||||
description 'Represents a project or group board'
|
||||
|
||||
accepts ::Board
|
||||
authorize :read_board
|
||||
|
||||
field :id, type: GraphQL::ID_TYPE, null: false,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ module Types
|
|||
# @param value [String]
|
||||
# @return [GID]
|
||||
def self.coerce_input(value, _ctx)
|
||||
return if value.nil?
|
||||
|
||||
gid = GlobalID.parse(value)
|
||||
raise GraphQL::CoercionError, "#{value.inspect} is not a valid Global ID" if gid.nil?
|
||||
raise GraphQL::CoercionError, "#{value.inspect} is not a Gitlab Global ID" unless gid.app == GlobalID.app
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ module BlobHelper
|
|||
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
|
||||
return unless blob = readable_blob(options, path, project, ref)
|
||||
|
||||
common_classes = "btn btn-primary js-edit-blob ml-2 #{options[:extra_class]}"
|
||||
common_classes = "btn btn-primary js-edit-blob gl-mr-3 #{options[:extra_class]}"
|
||||
data = { track_event: 'click_edit', track_label: 'Edit' }
|
||||
|
||||
if Feature.enabled?(:web_ide_primary_edit, project.group)
|
||||
|
|
@ -88,7 +88,7 @@ module BlobHelper
|
|||
def ide_edit_button(project = @project, ref = @ref, path = @path, blob:)
|
||||
return unless blob
|
||||
|
||||
common_classes = 'btn btn-primary ide-edit-button ml-2'
|
||||
common_classes = 'btn btn-primary ide-edit-button gl-mr-3'
|
||||
data = { track_event: 'click_edit_ide', track_label: 'Web IDE' }
|
||||
|
||||
unless Feature.enabled?(:web_ide_primary_edit, project.group)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ module GitlabRoutingHelper
|
|||
project_commit_url(entity.project, entity.sha, *args)
|
||||
end
|
||||
|
||||
def release_url(entity, *args)
|
||||
project_release_url(entity.project, entity, *args)
|
||||
end
|
||||
|
||||
def preview_markdown_path(parent, *args)
|
||||
return group_preview_markdown_path(parent, *args) if parent.is_a?(Group)
|
||||
|
||||
|
|
|
|||
|
|
@ -367,10 +367,10 @@ module SearchHelper
|
|||
end
|
||||
|
||||
# _search_highlight is used in EE override
|
||||
def highlight_and_truncate_issue(issue, search_term, _search_highlight)
|
||||
return unless issue.description.present?
|
||||
def highlight_and_truncate_issuable(issuable, search_term, _search_highlight)
|
||||
return unless issuable.description.present?
|
||||
|
||||
simple_search_highlight_and_truncate(issue.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
|
||||
simple_search_highlight_and_truncate(issuable.description, search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
|
||||
end
|
||||
|
||||
def show_user_search_tab?
|
||||
|
|
@ -380,6 +380,36 @@ module SearchHelper
|
|||
can?(current_user, :read_users_list)
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_state_to_badge_class(issuable)
|
||||
# Closed is considered "danger" for MR so we need to handle separately
|
||||
if issuable.is_a?(::MergeRequest)
|
||||
if issuable.merged?
|
||||
:primary
|
||||
elsif issuable.closed?
|
||||
:danger
|
||||
else
|
||||
:success
|
||||
end
|
||||
else
|
||||
if issuable.closed?
|
||||
:info
|
||||
else
|
||||
:success
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_state_text(issuable)
|
||||
case issuable.state
|
||||
when 'merged'
|
||||
_("Merged")
|
||||
when 'closed'
|
||||
_("Closed")
|
||||
else
|
||||
_("Open")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SearchHelper.prepend_if_ee('EE::SearchHelper')
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ module TriggerableHooks
|
|||
pipeline_hooks: :pipeline_events,
|
||||
wiki_page_hooks: :wiki_page_events,
|
||||
deployment_hooks: :deployment_events,
|
||||
feature_flag_hooks: :feature_flag_events
|
||||
feature_flag_hooks: :feature_flag_events,
|
||||
release_hooks: :releases_events
|
||||
}.freeze
|
||||
|
||||
extend ActiveSupport::Concern
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class ProjectHook < WebHook
|
|||
:pipeline_hooks,
|
||||
:wiki_page_hooks,
|
||||
:deployment_hooks,
|
||||
:feature_flag_hooks
|
||||
:feature_flag_hooks,
|
||||
:release_hooks
|
||||
]
|
||||
|
||||
belongs_to :project
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ class MergeRequest < ApplicationRecord
|
|||
scope :preload_author, -> { preload(:author) }
|
||||
scope :preload_approved_by_users, -> { preload(:approved_by_users) }
|
||||
scope :preload_metrics, -> (relation) { preload(metrics: relation) }
|
||||
scope :with_web_entity_associations, -> { preload(:author, :target_project) }
|
||||
|
||||
scope :with_auto_merge_enabled, -> do
|
||||
with_state(:opened).where(auto_merge_enabled: true)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ class Release < ApplicationRecord
|
|||
self.milestones.map {|m| m.title }.sort.join(", ")
|
||||
end
|
||||
|
||||
def to_hook_data(action)
|
||||
Gitlab::HookData::ReleaseBuilder.new(self).build(action)
|
||||
end
|
||||
|
||||
def execute_hooks(action)
|
||||
hook_data = to_hook_data(action)
|
||||
project.execute_hooks(hook_data, :release_hooks)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def actual_sha
|
||||
|
|
|
|||
|
|
@ -30,5 +30,15 @@ module Releases
|
|||
def external?
|
||||
!internal?
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
{
|
||||
id: id,
|
||||
external: external?,
|
||||
link_type: link_type,
|
||||
name: name,
|
||||
url: url
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ module Releases
|
|||
format: format)
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
{
|
||||
format: format,
|
||||
url: url
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def archive_prefix
|
||||
|
|
|
|||
|
|
@ -58,5 +58,12 @@ module Integrations
|
|||
|
||||
Gitlab::DataBuilder::Deployment.build(deployment)
|
||||
end
|
||||
|
||||
def releases_events_data
|
||||
release = project.releases.first
|
||||
return { error: s_('TestHooks|Ensure the project has releases.') } unless release.present?
|
||||
|
||||
release.to_hook_data('create')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ module Integrations
|
|||
wiki_page_events_data
|
||||
when 'deployment'
|
||||
deployment_events_data
|
||||
when 'release'
|
||||
releases_events_data
|
||||
else
|
||||
push_events_data
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ module Releases
|
|||
params.key?(:milestones)
|
||||
end
|
||||
|
||||
def execute_hooks(release, action = 'create')
|
||||
release.execute_hooks(action)
|
||||
end
|
||||
|
||||
# overridden in EE
|
||||
def project_group_id; end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ module Releases
|
|||
|
||||
notify_create_release(release)
|
||||
|
||||
execute_hooks(release, 'create')
|
||||
|
||||
create_evidence!(release, evidence_pipeline)
|
||||
|
||||
success(tag: tag, release: release)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@
|
|||
module Releases
|
||||
class UpdateService < Releases::BaseService
|
||||
def execute
|
||||
return error('Tag does not exist', 404) unless existing_tag
|
||||
return error('Release does not exist', 404) unless release
|
||||
return error('Access Denied', 403) unless allowed?
|
||||
return error('params is empty', 400) if empty_params?
|
||||
return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
|
||||
if error = validate
|
||||
return error
|
||||
end
|
||||
|
||||
if param_for_milestone_titles_provided?
|
||||
previous_milestones = release.milestones.map(&:title)
|
||||
|
|
@ -20,6 +18,7 @@ module Releases
|
|||
# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43385
|
||||
ActiveRecord::Base.transaction do
|
||||
if release.update(params)
|
||||
execute_hooks(release, 'update')
|
||||
success(tag: existing_tag, release: release, milestones_updated: milestones_updated?(previous_milestones))
|
||||
else
|
||||
error(release.errors.messages || '400 Bad request', 400)
|
||||
|
|
@ -31,6 +30,14 @@ module Releases
|
|||
|
||||
private
|
||||
|
||||
def validate
|
||||
return error('Tag does not exist', 404) unless existing_tag
|
||||
return error('Release does not exist', 404) unless release
|
||||
return error('Access Denied', 403) unless allowed?
|
||||
return error('params is empty', 400) if empty_params?
|
||||
return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
|
||||
end
|
||||
|
||||
def allowed?
|
||||
Ability.allowed?(current_user, :update_release, release)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ module TestHooks
|
|||
pipeline_events_data
|
||||
when 'wiki_page_events'
|
||||
wiki_page_events_data
|
||||
when 'releases_events'
|
||||
releases_events_data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
%section.settings.no-animate{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4= s_('ClusterIntegration|Provider details')
|
||||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
%button.btn.gl-button.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
|
||||
.settings-content
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
.form-actions
|
||||
- if @milestone.new_record?
|
||||
= f.submit 'Create milestone', class: "btn-success btn", data: { qa_selector: "create_milestone_button" }
|
||||
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
|
||||
= f.submit 'Create milestone', class: "btn-success gl-button btn", data: { qa_selector: "create_milestone_button" }
|
||||
= link_to "Cancel", group_milestones_path(@group), class: "btn gl-button btn-cancel"
|
||||
- else
|
||||
= f.submit 'Update milestone', class: "btn-success btn"
|
||||
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel"
|
||||
= f.submit 'Update milestone', class: "btn-success gl-button btn"
|
||||
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
= render 'shared/milestones/search_form'
|
||||
= render 'shared/milestones_sort_dropdown'
|
||||
- if can?(current_user, :admin_milestone, @group)
|
||||
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success", data: { qa_selector: "new_group_milestone_link" }
|
||||
= link_to "New milestone", new_group_milestone_path(@group), class: "btn gl-button btn-success", data: { qa_selector: "new_group_milestone_link" }
|
||||
|
||||
.milestones
|
||||
%ul.content-list
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
.js-file-title.file-title-flex-parent
|
||||
= render 'projects/blob/header_content', blob: blob
|
||||
|
||||
.file-actions<
|
||||
.file-actions.gl-display-flex.gl-flex-fill-1.gl-align-self-start.gl-md-justify-content-end<
|
||||
= render 'projects/blob/viewer_switcher', blob: blob unless blame
|
||||
- if Feature.enabled?(:consolidated_edit_button, @project)
|
||||
= render 'shared/web_ide_button', blob: blob
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.file-header-content
|
||||
= blob_icon blob.mode, blob.name
|
||||
|
||||
%strong.file-title-name{ data: { qa_selector: 'file_name_content' } }
|
||||
%strong.file-title-name.gl-word-break-all{ data: { qa_selector: 'file_name_content' } }
|
||||
= blob.name
|
||||
|
||||
= copy_file_path_button(blob.path)
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
|
||||
- if diff_file.renamed_file?
|
||||
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
|
||||
%strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
|
||||
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.old_path, container: 'body' } }
|
||||
= old_path
|
||||
→
|
||||
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
|
||||
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.new_path, container: 'body' } }
|
||||
= new_path
|
||||
- else
|
||||
%strong.file-title-name.has-tooltip{ data: { title: diff_file.file_path, container: 'body' } }
|
||||
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.file_path, container: 'body' } }
|
||||
= diff_file.file_path
|
||||
|
||||
- if diff_file.deleted_file?
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
= copy_file_path_button(diff_file.file_path)
|
||||
|
||||
- if diff_file.mode_changed?
|
||||
%small
|
||||
%small.gl-mr-2
|
||||
#{diff_file.a_mode} → #{diff_file.b_mode}
|
||||
|
||||
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
|
||||
%span.gl-display-flex.gl-align-items-center
|
||||
%span.badge.badge-pill.gl-badge.sm{ class: "badge-#{issuable_state_to_badge_class(issuable)}" }= issuable_state_text(issuable)
|
||||
= sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
|
||||
= link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
|
||||
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
|
||||
.gl-text-gray-500.gl-my-3
|
||||
= sprintf(s_(' %{project_name}#%{issuable_iid} · opened %{issuable_created} by %{author}'), { project_name: issuable.project.full_name, issuable_iid: issuable.iid, issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
|
||||
.description.term.col-sm-10.gl-px-0
|
||||
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
|
||||
|
|
@ -1,13 +1 @@
|
|||
%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
|
||||
%span.gl-display-flex.gl-align-items-center
|
||||
- if issue.closed?
|
||||
%span.badge.badge-info.badge-pill.gl-badge.sm= _("Closed")
|
||||
- else
|
||||
%span.badge.badge-success.badge-pill.gl-badge.sm= _("Open")
|
||||
= sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issue.confidential?
|
||||
= link_to project_issue_path(issue.project, issue), data: { track_event: 'click_text', track_label: 'issue_title', track_property: 'search_result' }, class: 'gl-w-full' do
|
||||
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
|
||||
.gl-text-gray-500.gl-my-3
|
||||
= sprintf(s_(' %{project_name}#%{issue_iid} · opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe
|
||||
.description.term.col-sm-10.gl-px-0
|
||||
= highlight_and_truncate_issue(issue, @search_term, @search_highlight)
|
||||
= render partial: 'search/results/issuable', object: issue
|
||||
|
|
|
|||
|
|
@ -1,14 +1 @@
|
|||
.search-result-row
|
||||
%h4
|
||||
= link_to project_merge_request_path(merge_request.target_project, merge_request), data: {track_event: 'click_text', track_label: 'merge_request_title', track_property: 'search_result'} do
|
||||
%span.term.str-truncated= merge_request.title
|
||||
- if merge_request.merged?
|
||||
%span.badge.badge-primary.gl-ml-2= _("Merged")
|
||||
- elsif merge_request.closed?
|
||||
%span.badge.badge-danger.gl-ml-2= _("Closed")
|
||||
.float-right= merge_request.to_reference
|
||||
- if merge_request.description.present?
|
||||
.description.term
|
||||
= search_md_sanitize(merge_request.description)
|
||||
%span.light
|
||||
#{merge_request.project.full_name}
|
||||
= render partial: 'search/results/issuable', object: merge_request
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@
|
|||
%strong= s_('Webhooks|Feature Flag events')
|
||||
%p.text-muted.ml-1
|
||||
= s_('Webhooks|This URL is triggered when a feature flag is turned on or off')
|
||||
%li
|
||||
= form.check_box :releases_events, class: 'form-check-input'
|
||||
= form.label :releases_events, class: 'list-label form-check-label ml-1' do
|
||||
%strong= s_('Webhooks|Releases events')
|
||||
%p.text-muted.ml-1
|
||||
= s_('Webhooks|This URL is triggered when a release is created/updated')
|
||||
.form-group
|
||||
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
|
||||
.form-check
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
database_unhealthy_counter.increment if lease_obtained && !healthy_db
|
||||
|
||||
# If we've tried several times to get a lease with a healthy DB without success, just give up.
|
||||
# Otherwise we could end up in an infinite rescheduling loop.
|
||||
# When the DB is unhealthy or the lease can't be obtained after several tries,
|
||||
# then give up on the job and log a warning. Otherwise we could end up in
|
||||
# an infinite rescheduling loop. Jobs can be tracked in the database with the
|
||||
# use of Gitlab::Database::BackgroundMigrationJob
|
||||
if !perform && attempts_left < 0
|
||||
msg = if !lease_obtained
|
||||
'Job could not get an exclusive lease after several tries. Giving up.'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make files header responsive and remove truncate name
|
||||
merge_request: 46406
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add webhooks for creating and updating a release
|
||||
merge_request: 44881
|
||||
author: David Barr @davebarr
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update merge request search results design
|
||||
merge_request: 46944
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddReleasesEventsToWebHooks < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :web_hooks, :releases_events, :boolean, null: false, default: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
f9bc943b61460b1a9a6db8189ab5b21eba46e14650c68658175299b14d48a030
|
||||
|
|
@ -17364,6 +17364,7 @@ CREATE TABLE web_hooks (
|
|||
encrypted_url character varying,
|
||||
encrypted_url_iv character varying,
|
||||
deployment_events boolean DEFAULT false NOT NULL,
|
||||
releases_events boolean DEFAULT false NOT NULL,
|
||||
feature_flag_events boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ to authenticate with the API:
|
|||
- [Maven Repository](../user/packages/maven_repository/index.md#authenticate-with-a-ci-job-token-in-maven)
|
||||
- [NPM Repository](../user/packages/npm_registry/index.md#authenticate-with-a-ci-job-token)
|
||||
- [Nuget Repository](../user/packages/nuget_repository/index.md)
|
||||
- [PyPI Repository](../user/packages/pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
|
||||
- [PyPI Repository](../user/packages/pypi_repository/index.md#authenticate-with-a-ci-job-token)
|
||||
- [Generic packages](../user/packages/generic_packages/index.md#publish-a-generic-package-by-using-cicd)
|
||||
- [Get job artifacts](job_artifacts.md#get-job-artifacts)
|
||||
- [Pipeline triggers](pipeline_triggers.md) (using the `token=` parameter)
|
||||
|
|
|
|||
|
|
@ -21475,7 +21475,7 @@ input UpdateIssueInput {
|
|||
"""
|
||||
The ID of the parent epic. NULL when removing the association
|
||||
"""
|
||||
epicId: ID
|
||||
epicId: EpicID
|
||||
|
||||
"""
|
||||
The desired health status
|
||||
|
|
|
|||
|
|
@ -62412,7 +62412,7 @@
|
|||
"description": "The ID of the parent epic. NULL when removing the association",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"name": "EpicID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,7 @@ GET /groups/:id/hooks/:hook_id
|
|||
"pipeline_events": true,
|
||||
"wiki_page_events": true,
|
||||
"deployment_events": true,
|
||||
"releases_events": true,
|
||||
"enable_ssl_verification": true,
|
||||
"created_at": "2012-10-12T17:04:47Z"
|
||||
}
|
||||
|
|
@ -1071,6 +1072,7 @@ POST /groups/:id/hooks
|
|||
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
|
||||
| `wiki_page_events` | boolean | no | Trigger hook on wiki events |
|
||||
| `deployment_events` | boolean | no | Trigger hook on deployment events |
|
||||
| `releases_events` | boolean | no | Trigger hook on release events |
|
||||
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
|
||||
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
|
||||
|
||||
|
|
@ -1098,6 +1100,7 @@ PUT /groups/:id/hooks/:hook_id
|
|||
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
|
||||
| `wiki_events` | boolean | no | Trigger hook on wiki events |
|
||||
| `deployment_events` | boolean | no | Trigger hook on deployment events |
|
||||
| `releases_events` | boolean | no | Trigger hook on release events |
|
||||
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
|
||||
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
|
||||
|
||||
|
|
|
|||
|
|
@ -2006,6 +2006,7 @@ GET /projects/:id/hooks/:hook_id
|
|||
"pipeline_events": true,
|
||||
"wiki_page_events": true,
|
||||
"deployment_events": true,
|
||||
"releases_events": true,
|
||||
"enable_ssl_verification": true,
|
||||
"created_at": "2012-10-12T17:04:47Z"
|
||||
}
|
||||
|
|
@ -2065,6 +2066,7 @@ PUT /projects/:id/hooks/:hook_id
|
|||
| `token` | string | **{dotted-circle}** No | Secret token to validate received payloads; this isn't returned in the response. |
|
||||
| `url` | string | **{check-circle}** Yes | The hook URL. |
|
||||
| `wiki_events` | boolean | **{dotted-circle}** No | Trigger hook on wiki events. |
|
||||
| `releases_events` | boolean | **{dotted-circle}** No | Trigger hook on release events. |
|
||||
|
||||
### Delete project hook
|
||||
|
||||
|
|
|
|||
|
|
@ -413,3 +413,78 @@ This basic GitOps example deploys NGINX:
|
|||
- [Configuration repository](https://gitlab.com/gitlab-org/configure/examples/kubernetes-agent)
|
||||
- [Manifest repository](https://gitlab.com/gitlab-org/configure/examples/gitops-project)
|
||||
- [Install GitLab Runner](https://gitlab.com/gitlab-examples/install-runner-via-k8s-agent)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you face any issues while using GitLab Kubernetes Agent, you can read the
|
||||
service logs with the following commands:
|
||||
|
||||
- KAS pod logs - Tail these logs with the
|
||||
`kubectl logs -f -l=app=kas -n <YOUR-GITLAB-NAMESPACE>`
|
||||
command. In Omnibus GitLab, the logs reside in `/var/log/gitlab/gitlab-kas/`.
|
||||
- Agent pod logs - Tail these logs with the
|
||||
`kubectl logs -f -l=app=gitlab-agent -n <YOUR-DESIRED-NAMESPACE>` command.
|
||||
|
||||
### KAS logs - GitOps: failed to get project info
|
||||
|
||||
```plaintext
|
||||
{"level":"warn","time":"2020-10-30T08:37:26.123Z","msg":"GitOps: failed to get project info","agent_id":4,"project_id":"root/kas-manifest001","error":"error kind: 0; status: 404"}
|
||||
```
|
||||
|
||||
This error is shown if the specified manifest project `root/kas-manifest001`
|
||||
doesn't exist, or if a project is private. To fix it, make sure the project exists
|
||||
and its visibility is [set to public](../../../public_access/public_access.md).
|
||||
|
||||
### KAS logs - Configuration file not found
|
||||
|
||||
```plaintext
|
||||
time="2020-10-29T04:44:14Z" level=warning msg="Config: failed to fetch" agent_id=2 error="configuration file not found: \".gitlab/agents/test-agent/config.yaml\
|
||||
```
|
||||
|
||||
This error is shown if the path to the configuration project was specified incorrectly,
|
||||
or if the path to `config.yaml` inside the project is not valid.
|
||||
|
||||
### Agent logs - Transport: Error while dialing failed to WebSocket dial
|
||||
|
||||
```plaintext
|
||||
{"level":"warn","time":"2020-11-04T10:14:39.368Z","msg":"GetConfiguration failed","error":"rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://gitlab-kas:443/-/kubernetes-agent\\\": dial tcp: lookup gitlab-kas on 10.60.0.10:53: no such host\""}
|
||||
```
|
||||
|
||||
This error is shown if there are some connectivity issues between the address
|
||||
specified as `kas-address`, and your Agent pod. To fix it, make sure that you
|
||||
specified the `kas-address` correctly.
|
||||
|
||||
### Agent logs - ValidationError(Deployment.metadata
|
||||
|
||||
```plaintext
|
||||
{"level":"info","time":"2020-10-30T08:56:54.329Z","msg":"Synced","project_id":"root/kas-manifest001","resource_key":"apps/Deployment/kas-test001/nginx-deployment","sync_result":"error validating data: [ValidationError(Deployment.metadata): unknown field \"replicas\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"selector\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"template\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta]"}
|
||||
```
|
||||
|
||||
This error is shown if your `manifest.yaml` file is malformed, and Kubernetes can't
|
||||
create specified objects. Make sure that your `manifest.yaml` file is valid. You
|
||||
may try using it to create objects in Kubernetes directly for more troubleshooting.
|
||||
|
||||
### Agent logs - Error while dialing failed to WebSocket dial: failed to send handshake request
|
||||
|
||||
```plaintext
|
||||
{"level":"warn","time":"2020-10-30T09:50:51.173Z","msg":"GetConfiguration failed","error":"rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent\\\": net/http: HTTP/1.x transport connection broken: malformed HTTP response \\\"\\\\x00\\\\x00\\\\x06\\\\x04\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x05\\\\x00\\\\x00@\\\\x00\\\"\""}
|
||||
```
|
||||
|
||||
This error is shown if you configured `wss` as `kas-address` on the agent side,
|
||||
but KAS on the server side is not available via `wss`. To fix it, make sure the
|
||||
same schemes are configured on both sides.
|
||||
|
||||
It's not possible to set the `grpc` scheme due to the issue
|
||||
[It is not possible to configure KAS to work with grpc without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
|
||||
issue is in progress, directly edit the deployment with the
|
||||
`kubectl edit deployment gitlab-kas` command, and change `--listen-websocket=true` to `--listen-websocket=false`. After running that command, you should be able to use
|
||||
`grpc://gitlab-kas.<YOUR-NAMESPACE>:5005`.
|
||||
|
||||
#### Agent logs - Decompressor is not installed for grpc-encoding
|
||||
|
||||
```plaintext
|
||||
{"level":"warn","time":"2020-11-05T05:25:46.916Z","msg":"GetConfiguration.Recv failed","error":"rpc error: code = Unimplemented desc = grpc: Decompressor is not installed for grpc-encoding \"gzip\""}
|
||||
```
|
||||
|
||||
This error is shown if the version of the agent is newer that the version of KAS.
|
||||
To fix it, make sure that both `agentk` and KAS use the same versions.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ Learn more about using CI/CD to build:
|
|||
- [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd)
|
||||
- [NuGet packages](../nuget_repository/index.md#publish-a-nuget-package-by-using-cicd)
|
||||
- [Conan packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd)
|
||||
- [PyPI packages](../pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
|
||||
- [Generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd)
|
||||
|
||||
If you use CI/CD to build a package, extended activity information is displayed
|
||||
|
|
|
|||
|
|
@ -12,67 +12,75 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
Publish PyPI packages in your project’s Package Registry. Then install the
|
||||
packages whenever you need to use them as a dependency.
|
||||
|
||||
The GitLab PyPI Repository works with:
|
||||
The Package Registry works with:
|
||||
|
||||
- [pip](https://pypi.org/project/pip/)
|
||||
- [twine](https://pypi.org/project/twine/)
|
||||
|
||||
## Build a PyPI package
|
||||
|
||||
This section covers creating a new example PyPI package to upload. This is a
|
||||
quickstart to test out the **GitLab PyPI Registry**. If you already understand
|
||||
how to build and publish your own packages, move on to the [next section](#authenticate-with-the-package-registry).
|
||||
This section explains how to create a PyPI package.
|
||||
|
||||
If you already use PyPI and know how to build your own packages, go to the
|
||||
[next section](#authenticate-with-the-package-registry).
|
||||
|
||||
### Install pip and twine
|
||||
|
||||
You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/).
|
||||
Install a recent version of [pip](https://pypi.org/project/pip/) and
|
||||
[twine](https://pypi.org/project/twine/).
|
||||
|
||||
### Create a project
|
||||
|
||||
Understanding how to create a full Python project is outside the scope of this
|
||||
guide, but you can create a small package to test out the registry. Start by
|
||||
creating a new directory called `MyPyPiPackage`:
|
||||
Create a test project.
|
||||
|
||||
```shell
|
||||
mkdir MyPyPiPackage && cd MyPyPiPackage
|
||||
```
|
||||
1. Open your terminal.
|
||||
1. Create a directory called `MyPyPiPackage`, and then go to that directory:
|
||||
|
||||
After creating this, create another directory inside:
|
||||
```shell
|
||||
mkdir MyPyPiPackage && cd MyPyPiPackage
|
||||
```
|
||||
|
||||
```shell
|
||||
mkdir mypypipackage && cd mypypipackage
|
||||
```
|
||||
1. Create another directory and go to it:
|
||||
|
||||
Create two new files inside this directory to set up the basic project:
|
||||
```shell
|
||||
mkdir mypypipackage && cd mypypipackage
|
||||
```
|
||||
|
||||
```shell
|
||||
touch __init__.py
|
||||
touch greet.py
|
||||
```
|
||||
1. Create the required files in this directory:
|
||||
|
||||
Inside `greet.py`, add the following code:
|
||||
```shell
|
||||
touch __init__.py
|
||||
touch greet.py
|
||||
```
|
||||
|
||||
```python
|
||||
def SayHello():
|
||||
print("Hello from MyPyPiPackage")
|
||||
return
|
||||
```
|
||||
1. Open the `greet.py` file, and then add:
|
||||
|
||||
Inside the `__init__.py` file, add the following:
|
||||
```python
|
||||
def SayHello():
|
||||
print("Hello from MyPyPiPackage")
|
||||
return
|
||||
```
|
||||
|
||||
```python
|
||||
from .greet import SayHello
|
||||
```
|
||||
1. Open the `__init__.py` file, and then add:
|
||||
|
||||
Now that the basics of our project is completed, we can test that the code runs.
|
||||
Start the Python prompt inside your top `MyPyPiPackage` directory. Then run:
|
||||
```python
|
||||
from .greet import SayHello
|
||||
```
|
||||
|
||||
```python
|
||||
>>> from mypypipackage import SayHello
|
||||
>>> SayHello()
|
||||
```
|
||||
1. To test the code, in your `MyPyPiPackage` directory, start the Python prompt.
|
||||
|
||||
You should see an output similar to the following:
|
||||
```shell
|
||||
python
|
||||
```
|
||||
|
||||
1. Run this command:
|
||||
|
||||
```python
|
||||
>>> from mypypipackage import SayHello
|
||||
>>> SayHello()
|
||||
```
|
||||
|
||||
A message indicates that the project was set up successfully:
|
||||
|
||||
```plaintext
|
||||
Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18)
|
||||
|
|
@ -83,83 +91,81 @@ Type "help", "copyright", "credits" or "license" for more information.
|
|||
Hello from MyPyPiPackage
|
||||
```
|
||||
|
||||
After we've verified that the sample project is working as previously described,
|
||||
we can next work on creating a package.
|
||||
|
||||
### Create a package
|
||||
|
||||
Inside your `MyPyPiPackage` directory, we need to create a `setup.py` file. Run
|
||||
the following:
|
||||
After you create a project, you can create a package.
|
||||
|
||||
```shell
|
||||
touch setup.py
|
||||
```
|
||||
1. In your terminal, go to the `MyPyPiPackage` directory.
|
||||
1. Create a `setup.py` file:
|
||||
|
||||
This file contains all the information about our package. For more information
|
||||
about this file, see [creating setup.py](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
|
||||
Becaue GitLab identifies packages based on
|
||||
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names),
|
||||
ensure your package name meets these requirements. See the [installation section](#publish-a-pypi-package-by-using-cicd)
|
||||
for more details.
|
||||
```shell
|
||||
touch setup.py
|
||||
```
|
||||
|
||||
For this guide, we don't need to extensively fill out this file. Add the
|
||||
following to your `setup.py`:
|
||||
This file contains all the information about the package. For more information
|
||||
about this file, see [creating setup.py](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
|
||||
Because GitLab identifies packages based on
|
||||
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names),
|
||||
ensure your package name meets these requirements. See the [installation section](#authenticate-with-a-ci-job-token)
|
||||
for details.
|
||||
|
||||
```python
|
||||
import setuptools
|
||||
1. Open the `setup.py` file, and then add basic information:
|
||||
|
||||
setuptools.setup(
|
||||
name="mypypipackage",
|
||||
version="0.0.1",
|
||||
author="Example Author",
|
||||
author_email="author@example.com",
|
||||
description="A small example package",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
)
|
||||
```
|
||||
```python
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
name="mypypipackage",
|
||||
version="0.0.1",
|
||||
author="Example Author",
|
||||
author_email="author@example.com",
|
||||
description="A small example package",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
)
|
||||
```
|
||||
|
||||
Save the file, and then execute the setup:
|
||||
1. Save the file.
|
||||
1. Execute the setup:
|
||||
|
||||
```shell
|
||||
python3 setup.py sdist bdist_wheel
|
||||
```
|
||||
```shell
|
||||
python3 setup.py sdist bdist_wheel
|
||||
```
|
||||
|
||||
If successful, you should be able to see the output in a newly created `dist`
|
||||
folder. Run:
|
||||
The output should be visible in a newly-created `dist` folder:
|
||||
|
||||
```shell
|
||||
ls dist
|
||||
```
|
||||
|
||||
And confirm your output matches the below:
|
||||
The output should appear similar to the following:
|
||||
|
||||
```plaintext
|
||||
mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz
|
||||
```
|
||||
|
||||
The package is now all set up and is ready to be uploaded to the
|
||||
_GitLab PyPI Package Registry_. Before we do so, we next need to set up
|
||||
authentication.
|
||||
The package is now ready to be published to the Package Registry.
|
||||
|
||||
## Authenticate with the Package Registry
|
||||
|
||||
Before you can publish to the Package Registry, you must authenticate.
|
||||
|
||||
To do this, you can use:
|
||||
|
||||
- A [personal access token](../../../user/profile/personal_access_tokens.md)
|
||||
with the scope set to `api`.
|
||||
- A [deploy token](./../../project/deploy_tokens/index.md) with the scope set to
|
||||
`read_package_registry`, `write_package_registry`, or both.
|
||||
- A [CI job token](#authenticate-with-a-ci-job-token).
|
||||
|
||||
### Authenticate with a personal access token
|
||||
|
||||
You need the following:
|
||||
|
||||
- A personal access token. You can generate a
|
||||
[personal access token](../../../user/profile/personal_access_tokens.md)
|
||||
with the scope set to `api` for repository authentication.
|
||||
- A suitable name for your source.
|
||||
- Your project ID, which is found on the home page of your project.
|
||||
|
||||
Edit your `~/.pypirc` file and add the following:
|
||||
To authenticate with a personal access token, edit the `~/.pypirc` file and add:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
|
|
@ -167,22 +173,17 @@ index-servers =
|
|||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/<project_id>/packages/pypi
|
||||
repository = https://gitlab.example.com/api/v4/projects/<project_id>/packages/pypi
|
||||
username = __token__
|
||||
password = <your personal access token>
|
||||
```
|
||||
|
||||
- `username` must be `__token__` exactly.
|
||||
- Your project ID is on your project's home page.
|
||||
|
||||
### Authenticate with a deploy token
|
||||
|
||||
You need the following:
|
||||
|
||||
- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md)
|
||||
with the `read_package_registry` or `write_package_registry` scopes for
|
||||
repository authentication.
|
||||
- A suitable name for your source.
|
||||
- Your project ID, which is found on the home page of your project.
|
||||
|
||||
Edit your `~/.pypirc` file and add the following:
|
||||
To authenticate with a deploy token, edit your `~/.pypirc` file and add:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
|
|
@ -190,22 +191,57 @@ index-servers =
|
|||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/<project_id>/packages/pypi
|
||||
repository = https://gitlab.example.com/api/v4/projects/<project_id>/packages/pypi
|
||||
username = <deploy token username>
|
||||
password = <deploy token>
|
||||
```
|
||||
|
||||
Your project ID is on your project's home page.
|
||||
|
||||
### Authenticate with a CI job token
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
|
||||
|
||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you
|
||||
can use `CI_JOB_TOKEN` instead of a personal access token or deploy token.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
image: python:latest
|
||||
|
||||
run:
|
||||
script:
|
||||
- pip install twine
|
||||
- python setup.py sdist bdist_wheel
|
||||
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
```
|
||||
|
||||
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check in to
|
||||
GitLab:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers =
|
||||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.example.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||
username = gitlab-ci-token
|
||||
password = ${env.CI_JOB_TOKEN}
|
||||
```
|
||||
|
||||
## Publish a PyPI package
|
||||
|
||||
When publishing packages, note that:
|
||||
|
||||
- The maximum allowed size is 50 Megabytes.
|
||||
- The maximum allowed size is 50 MB.
|
||||
- You can't upload the same version of a package multiple times. If you try,
|
||||
you'll receive the error `Validation failed: File name has already been taken`.
|
||||
|
||||
### Ensure your version string is valid
|
||||
|
||||
If your version string (for example, `0.0.1`) is invalid, it will be rejected.
|
||||
If your version string (for example, `0.0.1`) isn't valid, it will be rejected.
|
||||
GitLab uses the following regex to validate the version string.
|
||||
|
||||
```ruby
|
||||
|
|
@ -220,120 +256,86 @@ GitLab uses the following regex to validate the version string.
|
|||
)\z}xi
|
||||
```
|
||||
|
||||
You can experiment with the regex and try your version strings using this
|
||||
You can experiment with the regex and try your version strings by using this
|
||||
[regular expression editor](https://rubular.com/r/FKM6d07ouoDaFV).
|
||||
|
||||
For more details about the regex used, review this [documentation](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions).
|
||||
For more details about the regex, review this [documentation](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions).
|
||||
|
||||
### Publish a PyPI package by using twine
|
||||
|
||||
If you were following the steps on this page, the `MyPyPiPackage` package should
|
||||
be ready to be uploaded. Run the following command:
|
||||
To publish a PyPI package, run a command like:
|
||||
|
||||
```shell
|
||||
python3 -m twine upload --repository gitlab dist/*
|
||||
```
|
||||
|
||||
If successful, you should see the following:
|
||||
This message indicates that the package was published successfully:
|
||||
|
||||
```plaintext
|
||||
Uploading distributions to https://gitlab.com/api/v4/projects/<your_project_id>/packages/pypi
|
||||
Uploading distributions to https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/pypi
|
||||
Uploading mypypipackage-0.0.1-py3-none-any.whl
|
||||
100%|███████████████████████████████████████████████████████████████████████████████████████████| 4.58k/4.58k [00:00<00:00, 10.9kB/s]
|
||||
Uploading mypypipackage-0.0.1.tar.gz
|
||||
100%|███████████████████████████████████████████████████████████████████████████████████████████| 4.24k/4.24k [00:00<00:00, 11.0kB/s]
|
||||
```
|
||||
|
||||
This indicates that the package was uploaded successfully. You can then navigate
|
||||
to your project's **Packages & Registries** page and see the uploaded packages.
|
||||
To view the published package, go to your project's **Packages & Registries**
|
||||
page.
|
||||
|
||||
If you would rather not use a `.pypirc` file to define your repository source,
|
||||
you can upload to the repository with the authentication inline:
|
||||
If you didn't use a `.pypirc` file to define your repository source, you can
|
||||
publish to the repository with the authentication inline:
|
||||
|
||||
```shell
|
||||
TWINE_PASSWORD=<personal_access_token or deploy_token> TWINE_USERNAME=<username or deploy_token_username> python3 -m twine upload --repository-url https://gitlab.com/api/v4/projects/<project_id>/packages/pypi dist/*
|
||||
TWINE_PASSWORD=<personal_access_token or deploy_token> TWINE_USERNAME=<username or deploy_token_username> python3 -m twine upload --repository-url https://gitlab.example.com/api/v4/projects/<project_id>/packages/pypi dist/*
|
||||
```
|
||||
|
||||
If you didn't use the steps on this page, you need to ensure your package has
|
||||
been properly built, and that you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
|
||||
If you didn't follow the steps on this page, ensure your package was properly
|
||||
built, and that you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
|
||||
|
||||
You can then upload your package using the following command:
|
||||
You can then upload your package by using the following command:
|
||||
|
||||
```shell
|
||||
python -m twine upload --repository <source_name> dist/<package_file>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<package_file>` is your package filename, ending in `.tar.gz` or `.whl`.
|
||||
- `<source_name>` is the [source name used during setup](#authenticate-with-the-package-registry).
|
||||
|
||||
### Publish a PyPI package by using CI/CD
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
|
||||
|
||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you
|
||||
can use `CI_JOB_TOKEN` in place of the personal access token or deploy a token
|
||||
in your commands.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
image: python:latest
|
||||
|
||||
run:
|
||||
script:
|
||||
- pip install twine
|
||||
- python setup.py sdist bdist_wheel
|
||||
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
```
|
||||
|
||||
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers =
|
||||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||
username = gitlab-ci-token
|
||||
password = ${env.CI_JOB_TOKEN}
|
||||
```
|
||||
|
||||
## Install a PyPI package
|
||||
|
||||
Install the latest version of a package using the following command:
|
||||
To install the latest version of a package, use the following command:
|
||||
|
||||
```shell
|
||||
pip install --extra-index-url https://__token__:<personal_access_token>@gitlab.com/api/v4/projects/<project_id>/packages/pypi/simple --no-deps <package_name>
|
||||
pip install --extra-index-url https://__token__:<personal_access_token>@gitlab.example.com/api/v4/projects/<project_id>/packages/pypi/simple --no-deps <package_name>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<package_name>` is the package name.
|
||||
- `<personal_access_token>` is a personal access token with the `read_api` scope.
|
||||
- `<project_id>` is the project ID.
|
||||
|
||||
If you were following the guide above and want to test installing the
|
||||
`MyPyPiPackage` package, you can run the following:
|
||||
If you were following the guide and want to install the
|
||||
`MyPyPiPackage` package, you can run:
|
||||
|
||||
```shell
|
||||
pip install mypypipackage --no-deps --extra-index-url https://__token__:<personal_access_token>@gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/simple
|
||||
pip install mypypipackage --no-deps --extra-index-url https://__token__:<personal_access_token>@gitlab.example.com/api/v4/projects/<your_project_id>/packages/pypi/simple
|
||||
```
|
||||
|
||||
This should result in the following:
|
||||
This message indicates that the package was installed successfully:
|
||||
|
||||
```plaintext
|
||||
Looking in indexes: https://__token__:****@gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/simple
|
||||
Looking in indexes: https://__token__:****@gitlab.example.com/api/v4/projects/<your_project_id>/packages/pypi/simple
|
||||
Collecting mypypipackage
|
||||
Downloading https://gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/files/d53334205552a355fee8ca35a164512ef7334f33d309e60240d57073ee4386e6/mypypipackage-0.0.1-py3-none-any.whl (1.6 kB)
|
||||
Downloading https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/pypi/files/d53334205552a355fee8ca35a164512ef7334f33d309e60240d57073ee4386e6/mypypipackage-0.0.1-py3-none-any.whl (1.6 kB)
|
||||
Installing collected packages: mypypipackage
|
||||
Successfully installed mypypipackage-0.0.1
|
||||
```
|
||||
|
||||
GitLab looks for packages using
|
||||
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names),
|
||||
so the characters `-`, `_`, and `.` are all treated the same and repeated characters are removed.
|
||||
### Package names
|
||||
|
||||
GitLab looks for packages that use
|
||||
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names).
|
||||
The characters `-`, `_`, and `.` are all treated the same, and repeated
|
||||
characters are removed.
|
||||
|
||||
A `pip install` request for `my.package` looks for packages that match any of
|
||||
the three characters, such as `my-package`, `my_package`, and `my....package`.
|
||||
|
|
|
|||
|
|
@ -1407,6 +1407,91 @@ X-Gitlab-Event: Feature Flag Hook
|
|||
}
|
||||
```
|
||||
|
||||
### Release events
|
||||
|
||||
Triggered when a release is created or updated.
|
||||
|
||||
**Request Header**:
|
||||
|
||||
```plaintext
|
||||
X-Gitlab-Event: Release Hook
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"created_at": "2020-11-02 12:55:12 UTC",
|
||||
"description": "v1.0 has been released",
|
||||
"name": "v1.1",
|
||||
"released_at": "2020-11-02 12:55:12 UTC",
|
||||
"tag": "v1.1",
|
||||
"object_kind": "release",
|
||||
"project": {
|
||||
"id": 2,
|
||||
"name": "release-webhook-example",
|
||||
"description": "",
|
||||
"web_url": "https://example.com/gitlab-org/release-webhook-example",
|
||||
"avatar_url": null,
|
||||
"git_ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
|
||||
"git_http_url": "https://example.com/gitlab-org/release-webhook-example.git",
|
||||
"namespace": "Gitlab",
|
||||
"visibility_level": 0,
|
||||
"path_with_namespace": "gitlab-org/release-webhook-example",
|
||||
"default_branch": "master",
|
||||
"ci_config_path": null,
|
||||
"homepage": "https://example.com/gitlab-org/release-webhook-example",
|
||||
"url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
|
||||
"ssh_url": "ssh://git@example.com/gitlab-org/release-webhook-example.git",
|
||||
"http_url": "https://example.com/gitlab-org/release-webhook-example.git"
|
||||
},
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/releases/v1.1",
|
||||
"action": "create",
|
||||
"assets": {
|
||||
"count": 5,
|
||||
"links": [
|
||||
{
|
||||
"id": 1,
|
||||
"external": true,
|
||||
"link_type": "other",
|
||||
"name": "Changelog",
|
||||
"url": "https://example.net/changelog"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"format": "zip",
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.zip"
|
||||
},
|
||||
{
|
||||
"format": "tar.gz",
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.gz"
|
||||
},
|
||||
{
|
||||
"format": "tar.bz2",
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.bz2"
|
||||
},
|
||||
{
|
||||
"format": "tar",
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commit": {
|
||||
"id": "ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
|
||||
"message": "Release v1.1",
|
||||
"title": "Release v1.1",
|
||||
"timestamp": "2020-10-31T14:58:32+11:00",
|
||||
"url": "https://example.com/gitlab-org/release-webhook-example/-/commit/ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
|
||||
"author": {
|
||||
"name": "Example User",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Image URL rewriting
|
||||
|
||||
From GitLab 11.2, simple image references are rewritten to use an absolute URL
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ module API
|
|||
class ProjectHook < Hook
|
||||
expose :project_id, :issues_events, :confidential_issues_events
|
||||
expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events, :deployment_events
|
||||
expose :job_events
|
||||
expose :job_events, :releases_events
|
||||
expose :push_events_branch_filter
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ module API
|
|||
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
|
||||
optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
|
||||
optional :deployment_events, type: Boolean, desc: "Trigger hook on deployment events"
|
||||
optional :releases_events, type: Boolean, desc: "Trigger hook on release events"
|
||||
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
|
||||
optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
|
||||
optional :push_events_branch_filter, type: String, desc: "Trigger hook on specified branch only"
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ production_manual:
|
|||
when: never
|
||||
- if: '$CI_COMMIT_BRANCH != "master"'
|
||||
when: never
|
||||
# $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax
|
||||
# $INCREMENTAL_ROLLOUT_ENABLED is for compatibility with pre-GitLab 11.4 syntax
|
||||
- if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED'
|
||||
when: manual
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ production_manual:
|
|||
when: never
|
||||
- if: '$CI_COMMIT_BRANCH != "master"'
|
||||
when: never
|
||||
# $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax
|
||||
# $INCREMENTAL_ROLLOUT_ENABLED is for compatibility with pre-GitLab 11.4 syntax
|
||||
- if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED'
|
||||
when: manual
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module HookData
|
||||
class ReleaseBuilder < BaseBuilder
|
||||
def self.safe_hook_attributes
|
||||
%i[
|
||||
id
|
||||
created_at
|
||||
description
|
||||
name
|
||||
released_at
|
||||
tag
|
||||
].freeze
|
||||
end
|
||||
|
||||
alias_method :release, :object
|
||||
|
||||
def build(action)
|
||||
attrs = {
|
||||
object_kind: object_kind,
|
||||
project: release.project.hook_attrs,
|
||||
description: absolute_image_urls(release.description),
|
||||
url: Gitlab::UrlBuilder.build(release),
|
||||
action: action,
|
||||
assets: {
|
||||
count: release.assets_count,
|
||||
links: release.links.map(&:hook_attrs),
|
||||
sources: release.sources.map(&:hook_attrs)
|
||||
},
|
||||
commit: release.commit.hook_attrs
|
||||
}
|
||||
|
||||
release.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
|
||||
.merge!(attrs)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object_kind
|
||||
release.class.name.underscore
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,6 +32,8 @@ module Gitlab
|
|||
instance.milestone_url(object, **options)
|
||||
when Note
|
||||
note_url(object, **options)
|
||||
when Release
|
||||
instance.release_url(object, **options)
|
||||
when Project
|
||||
instance.project_url(object, **options)
|
||||
when Snippet
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
msgid " %{project_name}#%{issue_iid} · opened %{issue_created} by %{author}"
|
||||
msgid " %{project_name}#%{issuable_iid} · opened %{issuable_created} by %{author}"
|
||||
msgstr ""
|
||||
|
||||
msgid " %{start} to %{end}"
|
||||
|
|
@ -26495,6 +26495,9 @@ msgstr ""
|
|||
msgid "TestHooks|Ensure the project has notes."
|
||||
msgstr ""
|
||||
|
||||
msgid "TestHooks|Ensure the project has releases."
|
||||
msgstr ""
|
||||
|
||||
msgid "TestHooks|Ensure the wiki is enabled and has pages."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30125,6 +30128,9 @@ msgstr ""
|
|||
msgid "Webhooks|Push events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Releases events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|SSL verification"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30140,6 +30146,9 @@ msgstr ""
|
|||
msgid "Webhooks|This URL is triggered when a feature flag is turned on or off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|This URL is triggered when a release is created/updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|This URL will be triggered by a push to the repository"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ FactoryBot.define do
|
|||
wiki_page_events { true }
|
||||
deployment_events { true }
|
||||
feature_flag_events { true }
|
||||
releases_events { true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ RSpec.describe 'User views diffs', :js do
|
|||
end
|
||||
|
||||
it 'expands all diffs' do
|
||||
first('.js-file-title').click
|
||||
first('.diff-toggle-caret').click
|
||||
|
||||
expect(page).to have_button('Expand all')
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
|
|||
expect(page).to have_content('Merge requests events')
|
||||
expect(page).to have_content('Pipeline events')
|
||||
expect(page).to have_content('Wiki page events')
|
||||
expect(page).to have_content('Releases events')
|
||||
end
|
||||
|
||||
it 'create webhook' do
|
||||
|
|
|
|||
|
|
@ -45,8 +45,7 @@ RSpec.describe Types::GlobalIDType do
|
|||
end
|
||||
|
||||
it 'rejects nil' do
|
||||
expect { described_class.coerce_isolated_input(nil) }
|
||||
.to raise_error(GraphQL::CoercionError)
|
||||
expect(described_class.coerce_isolated_input(nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'rejects gids from different apps' do
|
||||
|
|
|
|||
|
|
@ -322,4 +322,14 @@ RSpec.describe GitlabRoutingHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'releases' do
|
||||
let(:release) { create(:release) }
|
||||
|
||||
describe '#release_url' do
|
||||
it 'returns the url for the release page' do
|
||||
expect(release_url(release)).to eq("http://test.host/#{release.project.full_path}/-/releases/#{release.tag}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ RSpec.describe SearchHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#highlight_and_truncate_issue' do
|
||||
describe '#highlight_and_truncate_issuable' do
|
||||
let(:description) { 'hello world' }
|
||||
let(:issue) { create(:issue, description: description) }
|
||||
let(:user) { create(:user) }
|
||||
|
|
@ -486,7 +486,7 @@ RSpec.describe SearchHelper do
|
|||
allow(self).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
subject { highlight_and_truncate_issue(issue, 'test', {}) }
|
||||
subject { highlight_and_truncate_issuable(issue, 'test', {}) }
|
||||
|
||||
context 'when description is not present' do
|
||||
let(:description) { nil }
|
||||
|
|
@ -545,4 +545,38 @@ RSpec.describe SearchHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issuable_state_to_badge_class' do
|
||||
context 'with merge request' do
|
||||
it 'returns correct badge based on status' do
|
||||
expect(issuable_state_to_badge_class(build(:merge_request, :merged))).to eq(:primary)
|
||||
expect(issuable_state_to_badge_class(build(:merge_request, :closed))).to eq(:danger)
|
||||
expect(issuable_state_to_badge_class(build(:merge_request, :opened))).to eq(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an issue' do
|
||||
it 'returns correct badge based on status' do
|
||||
expect(issuable_state_to_badge_class(build(:issue, :closed))).to eq(:info)
|
||||
expect(issuable_state_to_badge_class(build(:issue, :opened))).to eq(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issuable_state_text' do
|
||||
context 'with merge request' do
|
||||
it 'returns correct badge based on status' do
|
||||
expect(issuable_state_text(build(:merge_request, :merged))).to eq(_('Merged'))
|
||||
expect(issuable_state_text(build(:merge_request, :closed))).to eq(_('Closed'))
|
||||
expect(issuable_state_text(build(:merge_request, :opened))).to eq(_('Open'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an issue' do
|
||||
it 'returns correct badge based on status' do
|
||||
expect(issuable_state_text(build(:issue, :closed))).to eq(_('Closed'))
|
||||
expect(issuable_state_text(build(:issue, :opened))).to eq(_('Open'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::HookData::ReleaseBuilder do
|
||||
let_it_be(:project) { create(:project, :public, :repository) }
|
||||
let(:release) { create(:release, project: project) }
|
||||
let(:builder) { described_class.new(release) }
|
||||
|
||||
describe '#build' do
|
||||
let(:data) { builder.build('create') }
|
||||
|
||||
it 'includes safe attribute' do
|
||||
%w[
|
||||
id
|
||||
created_at
|
||||
description
|
||||
name
|
||||
released_at
|
||||
tag
|
||||
].each do |key|
|
||||
expect(data).to include(key)
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes additional attrs' do
|
||||
expect(data[:object_kind]).to eq('release')
|
||||
expect(data[:project]).to eq(builder.release.project.hook_attrs.with_indifferent_access)
|
||||
expect(data[:action]).to eq('create')
|
||||
expect(data).to include(:assets)
|
||||
expect(data).to include(:commit)
|
||||
end
|
||||
|
||||
context 'when the Release has an image in the description' do
|
||||
let(:release_with_description) do
|
||||
create(:release, project: project, description: 'test')
|
||||
end
|
||||
|
||||
let(:builder) { described_class.new(release_with_description) }
|
||||
|
||||
it 'sets the image to use an absolute URL' do
|
||||
expected_path = "#{release_with_description.project.path_with_namespace}/uploads/abc/Release_Image.png"
|
||||
|
||||
expect(data[:description])
|
||||
.to eq("test")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -61,6 +61,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory do
|
|||
'enable_ssl_verification' => true,
|
||||
'job_events' => false,
|
||||
'wiki_page_events' => true,
|
||||
'releases_events' => false,
|
||||
'token' => token
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -489,6 +489,7 @@ ProjectHook:
|
|||
- confidential_issues_events
|
||||
- confidential_note_events
|
||||
- repository_update_events
|
||||
- releases_events
|
||||
ProtectedBranch:
|
||||
- id
|
||||
- project_id
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ RSpec.describe Gitlab::UrlBuilder do
|
|||
:project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
|
||||
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
|
||||
:project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" }
|
||||
:release | ->(release) { "/#{release.project.full_path}/-/releases/#{release.tag}" }
|
||||
:ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" }
|
||||
:design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
|
|||
expect(json_response.first['pipeline_events']).to eq(true)
|
||||
expect(json_response.first['wiki_page_events']).to eq(true)
|
||||
expect(json_response.first['deployment_events']).to eq(true)
|
||||
expect(json_response.first['releases_events']).to eq(true)
|
||||
expect(json_response.first['enable_ssl_verification']).to eq(true)
|
||||
expect(json_response.first['push_events_branch_filter']).to eq('master')
|
||||
end
|
||||
|
|
@ -72,6 +73,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
|
|||
expect(json_response['job_events']).to eq(hook.job_events)
|
||||
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
|
||||
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
|
||||
expect(json_response['releases_events']).to eq(hook.releases_events)
|
||||
expect(json_response['deployment_events']).to eq(true)
|
||||
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
|
||||
end
|
||||
|
|
@ -97,7 +99,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
|
|||
post(api("/projects/#{project.id}/hooks", user),
|
||||
params: { url: "http://example.com", issues_events: true,
|
||||
confidential_issues_events: true, wiki_page_events: true,
|
||||
job_events: true, deployment_events: true,
|
||||
job_events: true, deployment_events: true, releases_events: true,
|
||||
push_events_branch_filter: 'some-feature-branch' })
|
||||
end.to change {project.hooks.count}.by(1)
|
||||
|
||||
|
|
@ -114,6 +116,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
|
|||
expect(json_response['pipeline_events']).to eq(false)
|
||||
expect(json_response['wiki_page_events']).to eq(true)
|
||||
expect(json_response['deployment_events']).to eq(true)
|
||||
expect(json_response['releases_events']).to eq(true)
|
||||
expect(json_response['enable_ssl_verification']).to eq(true)
|
||||
expect(json_response['push_events_branch_filter']).to eq('some-feature-branch')
|
||||
expect(json_response).not_to include('token')
|
||||
|
|
@ -169,6 +172,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do
|
|||
expect(json_response['job_events']).to eq(hook.job_events)
|
||||
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
|
||||
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
|
||||
expect(json_response['releases_events']).to eq(hook.releases_events)
|
||||
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -45,14 +45,9 @@ RSpec.describe SearchController, type: :request do
|
|||
let(:object) { :merge_request }
|
||||
let(:creation_args) { { source_project: project, title: 'bar' } }
|
||||
let(:params) { { search: 'bar', scope: 'merge_requests' } }
|
||||
# some N+1 queries exist
|
||||
# each merge request require 4 extra queries for:
|
||||
# - one for projects
|
||||
# - one for namespaces
|
||||
# - two for routes
|
||||
# plus 4 additional queries run for the logged in user:
|
||||
# there are 4 additional queries run for the logged in user:
|
||||
# - (1) geo_nodes, (1) users, (2) broadcast_messages
|
||||
let(:threshold) { 16 }
|
||||
let(:threshold) { 4 }
|
||||
|
||||
it_behaves_like 'an efficient database result'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ RSpec.describe Releases::CreateService do
|
|||
it 'creates a new release' do
|
||||
expected_job_count = MailScheduler::NotificationServiceWorker.jobs.size + 1
|
||||
|
||||
expect_next_instance_of(Release) do |release|
|
||||
expect(release)
|
||||
.to receive(:execute_hooks)
|
||||
.with('create')
|
||||
end
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(project.releases.count).to eq(1)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ RSpec.describe Releases::UpdateService do
|
|||
expect(result[:release].description).to eq(new_description)
|
||||
end
|
||||
|
||||
it 'executes hooks' do
|
||||
expect(service.release).to receive(:execute_hooks).with('update')
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
context 'when the tag does not exists' do
|
||||
let(:tag_name) { 'foobar' }
|
||||
|
||||
|
|
|
|||
|
|
@ -186,5 +186,23 @@ RSpec.describe TestHooks::ProjectService do
|
|||
expect(service.execute).to include(success_result)
|
||||
end
|
||||
end
|
||||
|
||||
context 'releases_events' do
|
||||
let(:trigger) { 'releases_events' }
|
||||
let(:trigger_key) { :release_hooks }
|
||||
|
||||
it 'returns error message if not enough data' do
|
||||
expect(hook).not_to receive(:execute)
|
||||
expect(service.execute).to include({ status: :error, message: 'Ensure the project has releases.' })
|
||||
end
|
||||
|
||||
it 'executes hook' do
|
||||
allow(project).to receive(:releases).and_return([Release.new])
|
||||
allow_any_instance_of(Release).to receive(:to_hook_data).and_return(sample_data)
|
||||
|
||||
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
|
||||
expect(service.execute).to include(success_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue