Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-07 00:08:58 +00:00
parent 63c5b4906b
commit b45d30ab76
70 changed files with 728 additions and 274 deletions

View File

@ -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!">

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -24,6 +24,13 @@ module Releases
format: format)
end
def hook_attrs
{
format: format,
url: url
}
end
private
def archive_prefix

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -52,6 +52,8 @@ module Releases
notify_create_release(release)
execute_hooks(release, 'create')
create_evidence!(release, evidence_pipeline)
success(tag: tag, release: release)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
&rarr;
%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

View File

@ -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} &middot; 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)

View File

@ -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} &middot; 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

View File

@ -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

View File

@ -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

View File

@ -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.'

View File

@ -0,0 +1,5 @@
---
title: Make files header responsive and remove truncate name
merge_request: 46406
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add webhooks for creating and updating a release
merge_request: 44881
author: David Barr @davebarr
type: added

View File

@ -0,0 +1,5 @@
---
title: Update merge request search results design
merge_request: 46944
author:
type: changed

View File

@ -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

View File

@ -0,0 +1 @@
f9bc943b61460b1a9a6db8189ab5b21eba46e14650c68658175299b14d48a030

View File

@ -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
);

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 |

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -12,67 +12,75 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Publish PyPI packages in your projects 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`.

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -16,7 +16,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid " %{project_name}#%{issue_iid} &middot; opened %{issue_created} by %{author}"
msgid " %{project_name}#%{issuable_iid} &middot; 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 ""

View File

@ -23,6 +23,7 @@ FactoryBot.define do
wiki_page_events { true }
deployment_events { true }
feature_flag_events { true }
releases_events { true }
end
end
end

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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![Release_Image](/uploads/abc/Release_Image.png)')
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![Release_Image](#{Settings.gitlab.url}/#{expected_path})")
end
end
end
end

View File

@ -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

View File

@ -489,6 +489,7 @@ ProjectHook:
- confidential_issues_events
- confidential_note_events
- repository_update_events
- releases_events
ProtectedBranch:
- id
- project_id

View File

@ -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" }

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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' }

View File

@ -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