Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-09 15:12:42 +00:00
parent b808458daa
commit e6a54b33a9
73 changed files with 770 additions and 248 deletions

View File

@ -96,6 +96,13 @@ retire-js-dependency_scanning:
gemnasium-python-dependency_scanning:
rules: !reference [".reports:rules:gemnasium-python-dependency_scanning", rules]
yarn-audit-dependency_scanning:
extends: .ds-analyzer
image: "registry.gitlab.com/gitlab-org/security-products/analyzers/npm-audit:1.4.0"
variables:
TOOL: yarn
rules: !reference [".reports:rules:yarn-audit-dependency_scanning", rules]
# Analyze dependencies for malicious behavior
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
.package_hunter-base:

View File

@ -167,6 +167,7 @@
.nodejs-patterns: &nodejs-patterns
- '{package.json,*/package.json,*/*/package.json}'
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
.python-patterns: &python-patterns
- '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
@ -373,10 +374,6 @@
- ".dockerignore"
- "qa/**/*"
.code-shell-patterns: &code-shell-patterns
- "bin/**/*"
- "tooling/**/*"
# .code-backstage-qa-patterns + .workhorse-patterns
.setup-test-env-patterns: &setup-test-env-patterns
- "{package.json,yarn.lock}"
@ -1487,6 +1484,12 @@
when: never
- changes: *python-patterns
.reports:rules:yarn-audit-dependency_scanning:
rules:
- if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/'
when: never
- changes: *nodejs-patterns
.reports:rules:schedule-dast:
rules:
- if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
@ -1779,13 +1782,6 @@
- changes: *code-backstage-qa-patterns
- changes: *startup-css-patterns
###############
# Shell rules #
###############
.shell:rules:
rules:
- changes: *code-shell-patterns
#######################
# Test metadata rules #
#######################

View File

@ -107,15 +107,3 @@ feature-flags-usage:
when: always
paths:
- tmp/feature_flags/
shellcheck:
extends:
- .default-retry
- .shell:rules
stage: lint
needs: []
image:
name: koalaman/shellcheck-alpine
entrypoint: [""]
script:
- tooling/bin/shellcheck

View File

@ -1,22 +1,37 @@
import Vue from 'vue';
import { parseBoolean } from './lib/utils/common_utils';
import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
export default () => {
const el = document.querySelector('.js-confirm-danger');
if (!el) return null;
const { phrase, buttonText, confirmDangerMessage } = el.dataset;
const {
removeFormId = null,
phrase,
buttonText,
buttonTestid = null,
confirmDangerMessage,
disabled = false,
} = el.dataset;
return new Vue({
el,
provide: {
confirmDangerMessage,
},
render: (createElement) =>
createElement(ConfirmDanger, {
props: {
phrase,
buttonText,
buttonTestid,
disabled: parseBoolean(disabled),
},
provide: {
confirmDangerMessage,
on: {
confirm: () => {
if (removeFormId) document.getElementById(removeFormId)?.submit();
},
},
}),
});

View File

@ -10,10 +10,12 @@ import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import setupTransferEdit from '~/transfer_edit';
import initConfirmDanger from '~/init_confirm_danger';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
initConfirmDangerModal();
initConfirmDanger();
initSettingsPanels();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),

View File

@ -10,7 +10,6 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@ -37,7 +36,6 @@ export default {
GlSprintf,
PipelineEditorMiniGraph,
},
mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
@ -172,11 +170,7 @@ export default {
</span>
</div>
<div class="gl-display-flex gl-flex-wrap">
<pipeline-editor-mini-graph
v-if="glFeatures.pipelineEditorMiniGraph"
:pipeline="pipeline"
v-on="$listeners"
/>
<pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
<gl-button
class="gl-mt-2 gl-md-mt-0"
target="_blank"

View File

@ -39,6 +39,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
toggleAttentionRequired(data) {
this.$emit('toggle-attention-required', data);
},
},
};
</script>
@ -58,7 +61,12 @@ export default {
</template>
</span>
<uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
<uncollapsed-assignee-list
v-else
:users="sortedAssigness"
:issuable-type="issuableType"
@toggle-attention-required="toggleAttentionRequired"
/>
</div>
</div>
</template>

View File

@ -32,6 +32,11 @@ export default {
return this.users.length === 0;
},
},
methods: {
toggleAttentionRequired(data) {
this.$emit('toggle-attention-required', data);
},
},
};
</script>
@ -61,6 +66,7 @@ export default {
:users="users"
:issuable-type="issuableType"
class="gl-text-gray-800 gl-mt-2 hide-collapsed"
@toggle-attention-required="toggleAttentionRequired"
/>
</div>
</template>

View File

@ -125,6 +125,9 @@ export default {
availability: this.assigneeAvailabilityStatus[username] || '',
}));
},
toggleAttentionRequired(data) {
this.mediator.toggleAttentionRequired('assignee', data);
},
},
};
</script>
@ -152,6 +155,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@assign-self="assignSelf"
@toggle-attention-required="toggleAttentionRequired"
/>
</div>
</template>

View File

@ -2,6 +2,7 @@
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import AttentionRequiredToggle from '../attention_required_toggle.vue';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
import UserNameWithStatus from './user_name_with_status.vue';
@ -9,6 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
AttentionRequiredToggle,
AssigneeAvatarLink,
UserNameWithStatus,
},
@ -80,6 +82,9 @@ export default {
}
return u?.status?.availability || '';
},
toggleAttentionRequired(data) {
this.$emit('toggle-attention-required', data);
},
},
};
</script>
@ -108,6 +113,12 @@ export default {
}"
class="gl-display-inline-block"
>
<attention-required-toggle
v-if="showVerticalList && user.can_update_merge_request"
:user="user"
type="assignee"
@toggle-attention-required="toggleAttentionRequired"
/>
<assignee-avatar-link
:user="user"
:issuable-type="issuableType"

View File

@ -0,0 +1,74 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
i18n: {
attentionRequiredReviewer: __('Request attention to review'),
attentionRequiredAssignee: __('Request attention'),
removeAttentionRequired: __('Remove attention request'),
},
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
type: {
type: String,
required: true,
},
user: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
};
},
computed: {
tooltipTitle() {
if (this.user.attention_required) {
return this.$options.i18n.removeAttentionRequired;
}
return this.type === 'reviewer'
? this.$options.i18n.attentionRequiredReviewer
: this.$options.i18n.attentionRequiredAssignee;
},
},
methods: {
toggleAttentionRequired() {
if (this.loading) return;
this.$root.$emit(BV_HIDE_TOOLTIP);
this.loading = true;
this.$emit('toggle-attention-required', {
user: this.user,
callback: this.toggleAttentionRequiredComplete,
});
},
toggleAttentionRequiredComplete() {
this.loading = false;
},
},
};
</script>
<template>
<span v-gl-tooltip.left.viewport="tooltipTitle">
<gl-button
:loading="loading"
:variant="user.attention_required ? 'warning' : 'default'"
:icon="user.attention_required ? 'star' : 'star-o'"
:aria-label="tooltipTitle"
size="small"
category="tertiary"
@click="toggleAttentionRequired"
/>
</span>
</template>

View File

@ -49,6 +49,9 @@ export default {
requestReview(data) {
this.$emit('request-review', data);
},
toggleAttentionRequired(data) {
this.$emit('toggle-attention-required', data);
},
},
};
</script>
@ -70,6 +73,7 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
@toggle-attention-required="toggleAttentionRequired"
/>
</div>
</div>

View File

@ -88,6 +88,9 @@ export default {
requestReview(data) {
this.mediator.requestReview(data);
},
toggleAttentionRequired(data) {
this.mediator.toggleAttentionRequired('reviewer', data);
},
},
};
</script>
@ -106,6 +109,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@request-review="requestReview"
@toggle-attention-required="toggleAttentionRequired"
/>
</div>
</template>

View File

@ -1,6 +1,8 @@
<script>
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf, s__ } from '~/locale';
import AttentionRequiredToggle from '../attention_required_toggle.vue';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const LOADING_STATE = 'loading';
@ -14,10 +16,12 @@ export default {
GlButton,
GlIcon,
ReviewerAvatarLink,
AttentionRequiredToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@ -76,6 +80,9 @@ export default {
this.loadingStates[userId] = null;
}
},
toggleAttentionRequired(data) {
this.$emit('toggle-attention-required', data);
},
},
LOADING_STATE,
SUCCESS_STATE,
@ -90,6 +97,12 @@ export default {
:class="{ 'gl-mb-3': index !== users.length - 1 }"
data-testid="reviewer"
>
<attention-required-toggle
v-if="glFeatures.mrAttentionRequests && user.can_update_merge_request"
:user="user"
type="reviewer"
@toggle-attention-required="toggleAttentionRequired"
/>
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
<span>{{ user.name }}</span>
@ -113,7 +126,9 @@ export default {
data-testid="re-request-success"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-else-if="
user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests
"
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"

View File

@ -0,0 +1,5 @@
mutation mergeRequestAttentionRequired($projectPath: ID!, $iid: String!, $userId: ID!) {
mergeRequestAttentionRequired(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
}

View File

@ -5,6 +5,7 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
import attentionRequiredMutation from '../queries/attention_required.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@ -90,4 +91,15 @@ export default class SidebarService {
},
});
}
attentionRequired(userId) {
return gqClient.mutate({
mutation: attentionRequiredMutation,
variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
}

View File

@ -1,6 +1,6 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
@ -56,13 +56,55 @@ export default class SidebarMediator {
return this.service
.requestReview(userId)
.then(() => {
this.store.updateReviewer(userId);
this.store.updateReviewer(userId, 'reviewed');
toast(__('Requested review'));
callback(userId, true);
})
.catch(() => callback(userId, false));
}
async toggleAttentionRequired(type, { user, callback }) {
try {
const isReviewer = type === 'reviewer';
const reviewerOrAssignee = isReviewer
? this.store.findReviewer(user)
: this.store.findAssignee(user);
if (reviewerOrAssignee.attention_required) {
toast(
sprintf(__('Removed attention request from @%{username}'), {
username: user.username,
}),
);
} else {
await this.service.attentionRequired(user.id);
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
}
if (isReviewer) {
this.store.updateReviewer(user.id, 'attention_required');
} else {
this.store.updateAssignee(user.id, 'attention_required');
}
callback();
} catch (error) {
callback();
createFlash({
message: sprintf(__('Updating the attention request for %{username} failed.'), {
username: user.username,
}),
error,
captureError: true,
actionConfig: {
title: __('Try again'),
clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
},
});
}
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}

View File

@ -82,11 +82,19 @@ export default class SidebarStore {
}
}
updateReviewer(id) {
updateAssignee(id, stateKey) {
const assignee = this.findAssignee({ id });
if (assignee) {
assignee[stateKey] = !assignee[stateKey];
}
}
updateReviewer(id, stateKey) {
const reviewer = this.findReviewer({ id });
if (reviewer) {
reviewer.reviewed = false;
reviewer[stateKey] = !reviewer[stateKey];
}
}

View File

@ -26,6 +26,11 @@ export default {
type: String,
required: true,
},
buttonTestid: {
type: String,
required: false,
default: 'confirm-danger-button',
},
},
modalId: CONFIRM_DANGER_MODAL_ID,
};
@ -37,7 +42,7 @@ export default {
class="gl-button"
variant="danger"
:disabled="disabled"
data-testid="confirm-danger-button"
:data-testid="buttonTestid"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal

View File

@ -93,6 +93,7 @@ export default {
</p>
<gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
<gl-form-input
id="confirm_name_input"
v-model="confirmationPhrase"
class="form-control"
data-testid="confirm-danger-input"

View File

@ -73,13 +73,23 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def upload_manifest
@group.dependency_proxy_manifests.create!(
attrs = {
file_name: manifest_file_name,
content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
digest: request.headers['Docker-Content-Digest'],
digest: request.headers[DependencyProxy::Manifest::DIGEST_HEADER],
file: params[:file],
size: params[:file].size
)
}
manifest = @group.dependency_proxy_manifests
.active
.find_by_file_name(manifest_file_name)
if manifest
manifest.update!(attrs)
else
@group.dependency_proxy_manifests.create!(attrs)
end
event_name = tracking_event_name(object_type: :manifest, from_cache: false)
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
@ -105,7 +115,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
def send_manifest(manifest, from_cache:)
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
manifest.touch
response.headers['Docker-Content-Digest'] = manifest.digest
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
response.headers['Content-Length'] = manifest.size
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
response.headers['Etag'] = "\"#{manifest.digest}\""

View File

@ -3,7 +3,6 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end

View File

@ -8,6 +8,7 @@ module Types
authorize :read_dependency_proxy
field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.'
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
field :file_name, GraphQL::Types::String, null: false, description: 'Name of the manifest.'

View File

@ -220,6 +220,10 @@ module Types
group.container_repositories.size
end
def dependency_proxy_manifests
group.dependency_proxy_manifests.order_id_desc
end
def dependency_proxy_image_count
group.dependency_proxy_manifests.count
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Groups
module SettingsHelper
include GroupsHelper
def group_settings_confirm_modal_data(group, remove_form_id = nil)
{
remove_form_id: remove_form_id,
button_text: _('Remove group'),
button_testid: 'remove-group-button',
disabled: group.paid?.to_s,
confirm_danger_message: remove_group_message(group),
phrase: group.full_path
}
end
end
end
Groups::SettingsHelper.prepend_mod_with('Groups::SettingsHelper')

View File

@ -139,8 +139,6 @@ module Clusters
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
scope :preload_elasticstack, -> { preload(:integration_elastic_stack) }
scope :preload_environments, -> { preload(:environments) }
scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }

View File

@ -8,12 +8,15 @@ class DependencyProxy::Manifest < ApplicationRecord
belongs_to :group
MAX_FILE_SIZE = 10.megabytes.freeze
DIGEST_HEADER = 'Docker-Content-Digest'
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
scope :order_id_desc, -> { reorder(id: :desc) }
mount_file_store_uploader DependencyProxy::FileUploader
def self.find_by_file_name_or_digest(file_name:, digest:)

View File

@ -204,6 +204,8 @@ class Issue < ApplicationRecord
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
issue.clear_closure_reason_references
end
end
@ -379,6 +381,11 @@ class Issue < ApplicationRecord
!duplicated_to_id.nil?
end
def clear_closure_reason_references
self.moved_to_id = nil
self.duplicated_to_id = nil
end
def can_move?(user, to_project = nil)
if to_project
return false unless user.can?(:admin_issue, to_project)

View File

@ -14,7 +14,10 @@ module DependencyProxy
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
if response.success?
success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
success(
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
content_type: response.headers['content-type']
)
else
error(response.body, response.code)
end

View File

@ -20,7 +20,13 @@ module DependencyProxy
file.write(response.body)
file.flush
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
yield(
success(
file: file,
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
content_type: response.headers['content-type']
)
)
ensure
file.close
file.unlink

View File

@ -1,3 +1,4 @@
- remove_form_id = 'js-remove-group-form'
= render 'groups/settings/export', group: @group
.sub-section
@ -26,6 +27,6 @@
= f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning'
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group
= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
= render_if_exists 'groups/settings/restore', group: @group
= render_if_exists 'groups/settings/immediately_remove', group: @group
= render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: remove_form_id

View File

@ -1,9 +1,11 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
.sub-section
%h4.danger-title= _('Remove group')
= form_tag(group, method: :delete) do
= form_tag(group, method: :delete, id: remove_form_id) do
%p
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
%br
%strong= _('Removed group can not be restored!')
= render 'groups/settings/remove_button', group: group
= render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id

View File

@ -1,5 +1,6 @@
- if group.adjourned_deletion?
= render_if_exists 'groups/settings/adjourned_deletion', group: group
- else
= render 'groups/settings/permanent_deletion', group: group
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.adjourned_deletion?
= render_if_exists 'groups/settings/adjourned_deletion', group: group, remove_form_id: remove_form_id
- else
= render 'groups/settings/permanent_deletion', group: group, remove_form_id: remove_form_id

View File

@ -1,7 +1,9 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.paid?
.gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
= button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-legacy-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' }
.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) }

View File

@ -6,9 +6,8 @@
= s_('WikiEmpty|Confluence is enabled')
%p
- wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629'
- wiki_confluence_epic_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, url: wiki_confluence_epic_link_url)
= format(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.").html_safe, wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe)
- wiki_confluence_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wiki_confluence_epic_link_url }
= html_escape(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.")) % { wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe }
= link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do
= sprite_icon('external-link')
= s_('WikiEmpty|Go to Confluence')
= sprite_icon('external-link')

View File

@ -1,12 +1,12 @@
#!/usr/bin/env bash
cd "$(dirname "$0")/.." || exit
cd $(dirname $0)/..
app_root=$(pwd)
sidekiq_workers=${SIDEKIQ_WORKERS:-1}
sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all)
sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid"
sidekiq_logfile="$app_root/log/sidekiq.log"
gitlab_user=$(ls -l config.ru | awk '{print $3}')
trap cleanup EXIT
@ -17,26 +17,26 @@ warn()
get_sidekiq_pid()
{
if [ ! -f "$sidekiq_pidfile" ]; then
if [ ! -f $sidekiq_pidfile ]; then
warn "No pidfile found at $sidekiq_pidfile; is Sidekiq running?"
return
fi
cat "$sidekiq_pidfile"
cat $sidekiq_pidfile
}
stop()
{
sidekiq_pid=$(get_sidekiq_pid)
if [ "$sidekiq_pid" ]; then
kill -TERM "$sidekiq_pid"
if [ $sidekiq_pid ]; then
kill -TERM $sidekiq_pid
fi
}
restart()
{
if [ -f "$sidekiq_pidfile" ]; then
if [ -f $sidekiq_pidfile ]; then
stop
fi
@ -53,12 +53,12 @@ start_sidekiq()
fi
# sidekiq-cluster expects an argument per process.
for (( i=1; i<=sidekiq_workers; i++ ))
for (( i=1; i<=$sidekiq_workers; i++ ))
do
processes_args+=("${sidekiq_queues}")
done
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P "$sidekiq_pidfile" -e "$RAILS_ENV" "$@" 2>&1 | tee -a "$sidekiq_logfile"
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@" 2>&1 | tee -a $sidekiq_logfile
}
cleanup()

View File

@ -1,6 +1,6 @@
#!/bin/sh
cd "$(dirname "$0")/.." || exit 1
cd $(dirname $0)/.. || exit 1
app_root=$(pwd)
mail_room_pidfile="$app_root/tmp/pids/mail_room.pid"
@ -9,7 +9,8 @@ mail_room_config="$app_root/config/mail_room.yml"
get_mail_room_pid()
{
pid=$(cat "$mail_room_pidfile")
local pid
pid=$(cat $mail_room_pidfile)
if [ -z "$pid" ] ; then
echo "Could not find a PID in $mail_room_pidfile"
exit 1
@ -19,13 +20,13 @@ get_mail_room_pid()
start()
{
bin/daemon_with_pidfile "$mail_room_pidfile" bundle exec mail_room --log-exit-as json -q -c "$mail_room_config" >> "$mail_room_logfile" 2>&1
bin/daemon_with_pidfile $mail_room_pidfile bundle exec mail_room --log-exit-as json -q -c $mail_room_config >> $mail_room_logfile 2>&1
}
stop()
{
get_mail_room_pid
kill -TERM "$mail_room_pid"
kill -TERM $mail_room_pid
}
restart()

View File

@ -32,20 +32,20 @@ if [ -z "$RSYNC" ] ; then
RSYNC=rsync
fi
if ! cd "$SRC" ; then
if ! cd $SRC ; then
echo "cd $SRC failed"
exit 1
fi
rsyncjob() {
relative_dir="./${1#"$SRC"}"
relative_dir="./${1#$SRC}"
if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then
echo "rsync $1 failed"
return 1
fi
echo "$1" >> "$LOGFILE"
echo "$1" >> $LOGFILE
}
export LOGFILE SRC DEST RSYNC

10
bin/web
View File

@ -2,7 +2,7 @@
set -e
cd "$(dirname "$0")/.."
cd $(dirname $0)/..
app_root=$(pwd)
puma_pidfile="$app_root/tmp/pids/puma.pid"
@ -25,12 +25,12 @@ get_puma_pid()
start()
{
spawn_puma "$@" &
spawn_puma &
}
start_foreground()
{
spawn_puma "$@"
spawn_puma
}
stop()
@ -46,10 +46,10 @@ reload()
case "$1" in
start)
start "$@"
start
;;
start_foreground)
start_foreground "$@"
start_foreground
;;
stop)
stop

View File

@ -10,7 +10,6 @@ shift
# Use set -a to export all variables defined in env_file.
set -a
# shellcheck disable=SC1090
. "${env_file}"
set +a

View File

@ -1,8 +0,0 @@
---
name: pipeline_editor_mini_graph
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217
milestone: '14.4'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -9237,6 +9237,7 @@ Dependency proxy manifest.
| <a id="dependencyproxymanifestcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="dependencyproxymanifestdigest"></a>`digest` | [`String!`](#string) | Digest of the manifest. |
| <a id="dependencyproxymanifestfilename"></a>`fileName` | [`String!`](#string) | Name of the manifest. |
| <a id="dependencyproxymanifestid"></a>`id` | [`DependencyProxyManifestID!`](#dependencyproxymanifestid) | ID of the manifest. |
| <a id="dependencyproxymanifestimagename"></a>`imageName` | [`String!`](#string) | Name of the image. |
| <a id="dependencyproxymanifestsize"></a>`size` | [`String!`](#string) | Size of the manifest file. |
| <a id="dependencyproxymanifestupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
@ -17280,6 +17281,12 @@ An example `DastSiteValidationID` is: `"gid://gitlab/DastSiteValidation/1"`.
Date represented in ISO 8601.
### `DependencyProxyManifestID`
A `DependencyProxyManifestID` is a global ID. It is encoded as a string.
An example `DependencyProxyManifestID` is: `"gid://gitlab/DependencyProxy::Manifest/1"`.
### `DesignManagementDesignAtVersionID`
A `DesignManagementDesignAtVersionID` is a global ID. It is encoded as a string.

View File

@ -1055,15 +1055,19 @@ Guidance for each individual UI element is in [the word list](word_list.md).
To be consistent, use this format when you write navigation steps in a task topic.
```markdown
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
```
Another example:
```markdown
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
```
An Admin Area example:
@ -1092,7 +1096,7 @@ For example:
If the UI text sufficiently explains the fields in a section, do not include a task step for every field.
Instead, summarize multiple fields in a single task step.
Use the phrase **Complete the fields**.
Use the phrase **Complete the fields**.
For example:

View File

@ -19,7 +19,7 @@ are accessible.
- **Jira Server**: Your network must allow access to your instance.
- **Jira Cloud**: Your instance must be accessible through the internet.
## Smart commits
## Smart Commits
When connecting GitLab with Jira with DVCS, you can process your Jira issues using
special commands, called
@ -48,17 +48,24 @@ Smart Commits should follow the pattern of:
Some examples:
- Adding a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.`
- Recording time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.`
- Closing an issue: `KEY-123 #close Closing issue`
- Add a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.`
- Record time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.`
- Close an issue: `KEY-123 #close Closing issue`
A Smart Commit message must not span more than one line (no carriage returns) but
you can still perform multiple actions in a single commit:
you can still perform multiple actions in a single commit. For example:
- Time tracking, commenting, and transitioning to **Closed**:
`KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close`.
- Commenting, transitioning to **In-progress**, and time tracking:
`KEY-123 #comment started working on the issue #in-progress #time 12d 5h`.
- Add time tracking, add a comment, and transition to **Closed**:
```plaintext
KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close
```
- Add a comment, transition to **In-progress**, and add time tracking:
```plaintext
KEY-123 #comment started working on the issue #in-progress #time 12d 5h
```
## Configure a GitLab application for DVCS
@ -69,9 +76,9 @@ you can set up this integration with your own account instead.
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
use to connect to GitLab. This user must be added to each project you want Jira to have access to,
or have an [Administrator](../../user/permissions.md) role to access all projects.
or be an administrator to access all projects.
1. Sign in as the `jira` user.
1. In the top right corner, click the account's avatar, and select **Edit profile**.
1. On the top bar, in the top right corner, select the user's avatar, and select **Edit profile**.
1. On the left sidebar, select **Applications**.
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab,
@ -86,10 +93,10 @@ you can set up this integration with your own account instead.
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
1. For **Scopes**, select `api` and clear any other checkboxes.
- The connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks.
- The DVCS connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks.
1. Select **Submit**.
1. GitLab displays the generated **Application ID**
and **Secret** values. Copy these values, as you need them to configure Jira.
1. Copy the **Application ID** and **Secret** values.
You need them to configure Jira.
## Configure Jira for DVCS
@ -97,19 +104,21 @@ Configure this connection when you want to import all GitLab commits and branche
for the groups you specify, into Jira. This import takes a few minutes and, after
it completes, refreshes every 60 minutes:
1. Ensure you have completed the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
1. Complete the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
1. Go to your DVCS accounts:
- *For Jira Server,* go to **Settings (gear) > Applications > DVCS accounts**.
- *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**.
- *For Jira Server,* select **Settings (gear) > Applications > DVCS accounts**.
- *For Jira Cloud,* select **Settings (gear) > Products > DVCS accounts**.
1. To create a new integration, select the appropriate value for **Host**:
- *For Jira versions 8.14 and later:* Select **GitLab** or
**GitLab Self-Managed**.
- *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**.
1. For **Team or User Account**, enter either:
- *For Jira versions 8.14 and later:*
- The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
- The relative path of a top-level GitLab group that
[the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
- *For Jira versions 8.13 and earlier:*
- The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
- The relative path of a top-level GitLab group that
[the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
- The relative path of your personal namespace.
1. In the **Host URL** field, enter the URI appropriate for your version of GitLab,
@ -120,13 +129,13 @@ it completes, refreshes every 60 minutes:
1. For **Client ID**, use the **Application ID** value from the previous section.
1. For **Client Secret**, use the **Secret** value from the previous section.
1. Ensure that the rest of the checkboxes are checked.
1. Select **Add** and then **Continue** to create the DVCS account.
1. Jira redirects to GitLab where you have to confirm the authorization,
and then GitLab redirects back to Jira where you should see the synced
projects show up inside the new account.
1. Ensure that the rest of the checkboxes are selected.
1. To create the DVCS account, select **Add** and then **Continue**.
1. Jira redirects to GitLab where you have to confirm the authorization.
GitLab then redirects back to Jira where the synced
projects should display in the new account.
To connect additional GitLab projects from other GitLab top-level groups, or
To connect additional GitLab projects from other GitLab top-level groups or
personal namespaces, repeat the previous steps with additional Jira DVCS accounts.
After you configure the integration, read more about [how to test and use it](development_panel.md).
@ -172,9 +181,8 @@ Error obtaining access token. Cannot access https://gitlab.example.com from Jira
as GitLab is the TLS client.
- The Jira Development panel integration requires Jira to connect to GitLab, which
causes Jira to be the TLS client. If your GitLab server's certificate is not
issued by a public certificate authority, the Java Truststore on Jira's server
must have the appropriate certificate (such as your organization's
root certificate) added to it .
issued by a public certificate authority, add the appropriate certificate
(such as your organization's root certificate) to the Java Truststore on Jira's server.
Refer to Atlassian's documentation and Atlassian Support for assistance setting
up Jira correctly:
@ -187,8 +195,8 @@ up Jira correctly:
- If the integration stops working after upgrading Jira's Java runtime, the
`cacerts` Truststore may have been replaced during the upgrade.
- Troubleshooting connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
using the a java class called `SSLPoke`.
- Troubleshoot connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
using the `SSLPoke` Java class.
- Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`.
- Use the same Java runtime as Jira.
- Pass all networking-related parameters that Jira is called with, such as proxy
@ -203,7 +211,7 @@ The message `Successfully connected` indicates a successful TLS handshake.
If there are problems, the Java TLS library generates errors that you can
look up for more detail.
### Scope error when connecting Jira via DVCS
### Scope error when connecting to Jira using DVCS
```plaintext
The requested scope is invalid, unknown, or malformed.
@ -224,12 +232,12 @@ After you complete the **Add New Account** form in Jira and authorize access, yo
encounter these issues:
- An `Error! Failed adding the account: [Error retrieving list of repositories]` error.
- An `Account is already integrated with JIRA` error when you click **Try Again**.
- An `Account is already integrated with JIRA` error when you select **Try Again**.
- An account is visible in the DVCS accounts view, but no repositories are listed.
To resolve this issue:
- If you're using GitLab Free, be sure you're using GitLab 13.4 or later.
- If you're using GitLab Free, ensure you're using GitLab 13.4 or later.
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
@ -243,17 +251,17 @@ This issue occurs when you use the Jira DVCS connector and your integration is c
For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
### Fix synchronization issues
### Synchronization issues
If Jira displays incorrect information, such as deleted branches, you may have to
resynchronize the information. To do so:
resynchronize the information:
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.
1. At the account (group or subgroup) level, Jira displays an option to
**Refresh repositories** in the **{ellipsis_h}** (ellipsis) menu.
1. For each project, there's a sync button displayed next to the **last activity** date.
- To perform a *soft resync*, click the button.
- To complete a *full sync*, shift-click the button.
1. In Jira, select **Jira Administration > Applications > DVCS accounts**.
1. For the account (group or subgroup), select
**Refresh repositories** from the **{ellipsis_h}** (ellipsis) menu.
1. For each project, next to the **Last activity** date:
- To perform a *soft resync*, select the sync icon.
- To complete a *full sync*, press `Shift` and select the sync icon.
For more information, read
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).

View File

@ -90,7 +90,7 @@ module ContainerRegistry
def repository_tag_digest(name, reference)
response = faraday.head("/v2/#{name}/manifests/#{reference}")
response.headers['docker-content-digest'] if response.success?
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
end
def delete_repository_tag_by_digest(name, reference)
@ -171,7 +171,7 @@ module ContainerRegistry
req.body = Gitlab::Json.pretty_generate(manifest)
end
response.headers['docker-content-digest'] if response.success?
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
end
private

View File

@ -49,6 +49,11 @@ module Gitlab
end
def offline!
::Gitlab::Database::LoadBalancing::Logger.warn(
event: :host_offline,
message: 'Marking primary host as offline'
)
nil
end

View File

@ -7,7 +7,7 @@ module Gitlab
def query(query)
client_query(query)
{ valid: true }
rescue Gitlab::PrometheusClient::QueryError, Gitlab::HTTP::BlockedUrlError => ex
rescue Gitlab::PrometheusClient::QueryError, Gitlab::PrometheusClient::ConnectionError => ex
{ valid: false, error: ex.message }
end

View File

@ -151,12 +151,8 @@ module Gitlab
def get(path, args)
Gitlab::HTTP.get(path, { query: args }.merge(http_options) )
rescue SocketError
raise PrometheusClient::ConnectionError, "Can't connect to #{api_url}"
rescue OpenSSL::SSL::SSLError
raise PrometheusClient::ConnectionError, "#{api_url} contains invalid SSL data"
rescue Errno::ECONNREFUSED
raise PrometheusClient::ConnectionError, 'Connection refused'
rescue *Gitlab::HTTP::HTTP_ERRORS => e
raise PrometheusClient::ConnectionError, e.message
end
def handle_management_api_response(response)

View File

@ -37,6 +37,11 @@ module Sidebars
def render?
context.project.has_confluence?
end
override :active_routes
def active_routes
{ controller: :confluences }
end
end
end
end

View File

@ -28636,6 +28636,9 @@ msgstr ""
msgid "Remove assignee"
msgstr ""
msgid "Remove attention request"
msgstr ""
msgid "Remove avatar"
msgstr ""
@ -28771,6 +28774,9 @@ msgstr ""
msgid "Removed an issue from an epic."
msgstr ""
msgid "Removed attention request from @%{username}"
msgstr ""
msgid "Removed group can not be restored!"
msgstr ""
@ -29199,6 +29205,12 @@ msgstr ""
msgid "Request a new one"
msgstr ""
msgid "Request attention"
msgstr ""
msgid "Request attention to review"
msgstr ""
msgid "Request details"
msgstr ""
@ -29220,6 +29232,9 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
msgid "Requested attention from @%{username}"
msgstr ""
msgid "Requested design version does not exist."
msgstr ""
@ -30909,7 +30924,7 @@ msgstr ""
msgid "SecurityReports|Take survey"
msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the lastest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
@ -36990,6 +37005,9 @@ msgstr ""
msgid "Updating"
msgstr ""
msgid "Updating the attention request for %{username} failed."
msgstr ""
msgid "Updating…"
msgstr ""

View File

@ -425,28 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
describe 'GET #authorize_upload_blob' do
describe 'POST #authorize_upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
post :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
end
it_behaves_like 'without permission'
it_behaves_like 'authorize action with permission'
end
describe 'GET #upload_blob' do
describe 'POST #upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :upload_blob, params: {
post :upload_blob, params: {
group_id: group.to_param,
image: 'alpine',
sha: blob_sha,
@ -469,31 +469,45 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
describe 'GET #authorize_upload_manifest' do
describe 'POST #authorize_upload_manifest' do
let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
post :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
end
it_behaves_like 'without permission'
it_behaves_like 'authorize action with permission'
end
describe 'GET #upload_manifest' do
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
describe 'POST #upload_manifest' do
let_it_be(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
let_it_be(:image) { 'alpine' }
let_it_be(:tag) { 'latest' }
let_it_be(:content_type) { 'v2/manifest' }
let_it_be(:digest) { 'foo' }
let_it_be(:file_name) { "#{image}:#{tag}.json" }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
get :upload_manifest, params: {
request.headers.merge!(
workhorse_internal_api_request_header.merge!(
{
Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER => content_type,
DependencyProxy::Manifest::DIGEST_HEADER => digest
}
)
)
params = {
group_id: group.to_param,
image: 'alpine',
tag: 'latest',
file: file
image: image,
tag: tag,
file: file,
file_name: file_name
}
post :upload_manifest, params: params
end
it_behaves_like 'without permission'
@ -501,13 +515,30 @@ RSpec.describe Groups::DependencyProxyForContainersController do
context 'with a valid user' do
before do
group.add_guest(user)
expect_next_found_instance_of(Group) do |instance|
expect(instance).to receive_message_chain(:dependency_proxy_manifests, :create!)
end
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
context 'with no existing manifest' do
it 'creates a manifest' do
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
manifest = group.dependency_proxy_manifests.first.reload
expect(manifest.content_type).to eq(content_type)
expect(manifest.digest).to eq(digest)
expect(manifest.file_name).to eq(file_name)
end
end
context 'with existing stale manifest' do
let_it_be(:old_digest) { 'asdf' }
let_it_be_with_reload(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, digest: old_digest, group: group) }
it 'updates the existing manifest' do
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(0)
.and change { manifest.reload.digest }.from(old_digest).to(digest)
end
end
end
end

View File

@ -16,9 +16,12 @@ RSpec.describe 'User views the Confluence page' do
visit project_wikis_confluence_path(project)
expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first)
element = page.find('.row.empty-state')
expect(element).to have_link('Go to Confluence', href: service.confluence_url)
expect(element).to have_link('Confluence epic', href: 'https://gitlab.com/groups/gitlab-org/-/epics/3629')
end
it 'does not show the page when the Confluence integration disabled' do

View File

@ -16,7 +16,7 @@ describe('Pipeline Status', () => {
let mockApollo;
let mockPipelineQuery;
const createComponentWithApollo = (glFeatures = {}) => {
const createComponentWithApollo = () => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@ -27,7 +27,6 @@ describe('Pipeline Status', () => {
commitSha: mockCommitSha,
},
provide: {
glFeatures,
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
@ -106,8 +105,8 @@ describe('Pipeline Status', () => {
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
});
it('does not render the pipeline mini graph', () => {
expect(findPipelineEditorMiniGraph().exists()).toBe(false);
it('renders the pipeline mini graph', () => {
expect(findPipelineEditorMiniGraph().exists()).toBe(true);
});
});
@ -150,19 +149,4 @@ describe('Pipeline Status', () => {
});
});
});
describe('when feature flag for pipeline mini graph is enabled', () => {
beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
data: { project: mockProjectPipeline() },
});
createComponentWithApollo({ pipelineEditorMiniGraph: true });
waitForPromises();
});
it('renders the pipeline mini graph', () => {
expect(findPipelineEditorMiniGraph().exists()).toBe(true);
});
});
});

View File

@ -0,0 +1,84 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue';
let wrapper;
function factory(propsData = {}) {
wrapper = mount(AttentionRequiredToggle, { propsData });
}
const findToggle = () => wrapper.findComponent(GlButton);
describe('Attention require toggle', () => {
afterEach(() => {
wrapper.destroy();
});
it('renders button', () => {
factory({ type: 'reviewer', user: { attention_required: false } });
expect(findToggle().exists()).toBe(true);
});
it.each`
attentionRequired | icon
${true} | ${'star'}
${false} | ${'star-o'}
`(
'renders $icon icon when attention_required is $attentionRequired',
({ attentionRequired, icon }) => {
factory({ type: 'reviewer', user: { attention_required: attentionRequired } });
expect(findToggle().props('icon')).toBe(icon);
},
);
it.each`
attentionRequired | variant
${true} | ${'warning'}
${false} | ${'default'}
`(
'renders button with variant $variant when attention_required is $attentionRequired',
({ attentionRequired, variant }) => {
factory({ type: 'reviewer', user: { attention_required: attentionRequired } });
expect(findToggle().props('variant')).toBe(variant);
},
);
it('emits toggle-attention-required on click', async () => {
factory({ type: 'reviewer', user: { attention_required: true } });
await findToggle().trigger('click');
expect(wrapper.emitted('toggle-attention-required')[0]).toEqual([
{
user: { attention_required: true },
callback: expect.anything(),
},
]);
});
it('sets loading on click', async () => {
factory({ type: 'reviewer', user: { attention_required: true } });
await findToggle().trigger('click');
expect(findToggle().props('loading')).toBe(true);
});
it.each`
type | attentionRequired | tooltip
${'reviewer'} | ${true} | ${AttentionRequiredToggle.i18n.removeAttentionRequired}
${'reviewer'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredReviewer}
${'assignee'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredAssignee}
`(
'sets tooltip as $tooltip when attention_required is $attentionRequired and type is $type',
({ type, attentionRequired, tooltip }) => {
factory({ type, user: { attention_required: attentionRequired } });
expect(findToggle().attributes('aria-label')).toBe(tooltip);
},
);
});

View File

@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
import userDataMock from '../../user_data_mock';
@ -9,7 +10,7 @@ describe('UncollapsedReviewerList component', () => {
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
function createComponent(props = {}) {
function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@ -18,6 +19,9 @@ describe('UncollapsedReviewerList component', () => {
wrapper = shallowMount(UncollapsedReviewerList, {
propsData,
provide: {
glFeatures,
},
});
}
@ -110,4 +114,18 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
});
});
it('hides re-request review button when attentionRequired feature flag is enabled', () => {
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
});
it('emits toggle-attention-required', () => {
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
wrapper.find(AttentionRequiredToggle).vm.$emit('toggle-attention-required', 'data');
expect(wrapper.emitted('toggle-attention-required')[0]).toEqual(['data']);
});
});

View File

@ -4,8 +4,11 @@ import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import toast from '~/vue_shared/plugins/global_toast';
import Mock from './mock_data';
jest.mock('~/vue_shared/plugins/global_toast');
describe('Sidebar mediator', () => {
const { mediator: mediatorMockData } = Mock;
let mock;
@ -115,4 +118,56 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
describe('toggleAttentionRequired', () => {
let attentionRequiredService;
beforeEach(() => {
attentionRequiredService = jest
.spyOn(mediator.service, 'attentionRequired')
.mockResolvedValue();
});
it('calls attentionRequired service method', async () => {
mediator.store.reviewers = [{ id: 1, attention_required: false, username: 'root' }];
await mediator.toggleAttentionRequired('reviewer', {
user: { id: 1, username: 'root' },
callback: jest.fn(),
});
expect(attentionRequiredService).toHaveBeenCalledWith(1);
});
it.each`
type | method
${'reviewer'} | ${'findReviewer'}
`('finds $type', ({ type, method }) => {
const methodSpy = jest.spyOn(mediator.store, method);
mediator.toggleAttentionRequired(type, { user: { id: 1 }, callback: jest.fn() });
expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
});
it.each`
attentionRequired | toastMessage
${true} | ${'Removed attention request from @root'}
${false} | ${'Requested attention from @root'}
`(
'it creates toast $toastMessage when attention_required is $attentionRequired',
async ({ attentionRequired, toastMessage }) => {
mediator.store.reviewers = [
{ id: 1, attention_required: attentionRequired, username: 'root' },
];
await mediator.toggleAttentionRequired('reviewer', {
user: { id: 1, username: 'root' },
callback: jest.fn(),
});
expect(toast).toHaveBeenCalledWith(toastMessage);
},
);
});
});

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['DependencyProxyManifest'] do
it 'includes dependency proxy manifest fields' do
expected_fields = %w[
file_name image_name size created_at updated_at digest
id file_name image_name size created_at updated_at digest
]
expect(described_class).to include_graphql_fields(*expected_fields)

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::SettingsHelper do
include GroupsHelper
let_it_be(:group) { create(:group, path: "foo") }
describe('#group_settings_confirm_modal_data') do
using RSpec::Parameterized::TableSyntax
fake_form_id = "fake_form_id"
where(:is_paid, :is_button_disabled, :form_value_id) do
true | "true" | nil
true | "true" | fake_form_id
false | "false" | nil
false | "false" | fake_form_id
end
with_them do
it "returns expected parameters" do
allow(group).to receive(:paid?).and_return(is_paid)
expected = helper.group_settings_confirm_modal_data(group, form_value_id)
expect(expected).to eq({
button_text: "Remove group",
confirm_danger_message: remove_group_message(group),
remove_form_id: form_value_id,
phrase: group.full_path,
button_testid: "remove-group-button",
disabled: is_button_disabled
})
end
end
end
end

View File

@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do
it 'uploads the manifest and returns the digest' do
stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
.with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers)
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' })
.to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:123' })
expect_new_faraday(timeout: false)

View File

@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do
before do
stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
.with(headers: headers)
.to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
.to_return(status: 200, headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:digest' })
end
describe '#digest' do

View File

@ -51,7 +51,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end
describe '#offline!' do
it 'does nothing' do
it 'logs the event but does nothing else' do
expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
.with(hash_including(event: :host_offline))
.and_call_original
expect(host.offline!).to be_nil
end
end

View File

@ -107,36 +107,14 @@ RSpec.describe Gitlab::PrometheusClient do
let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
shared_examples 'exceptions are raised' do
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
Gitlab::HTTP::HTTP_ERRORS.each do |error|
it "raises a Gitlab::PrometheusClient::ConnectionError when a #{error} is rescued" do
req_stub = stub_prometheus_request_with_exception(prometheus_url, error.new)
expect { subject }
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested
end
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
expect { subject }
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested
end
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError)
expect { subject }
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
expect(req_stub).to have_been_requested
end
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError with a code is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError.new(code: 400))
expect { subject }
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
expect(req_stub).to have_been_requested
expect { subject }
.to raise_error(Gitlab::PrometheusClient::ConnectionError, kind_of(String))
expect(req_stub).to have_been_requested
end
end
end

View File

@ -165,6 +165,14 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
end
end
context "when client raises Gitlab::PrometheusClient::ConnectionError" do
before do
stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError)
end
it { is_expected.to include(success: false, result: kind_of(String)) }
end
end
describe '#build_query_args' do

View File

@ -15,6 +15,17 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
it { is_expected.to validate_presence_of(:digest) }
end
describe 'scopes' do
let_it_be(:manifest_one) { create(:dependency_proxy_manifest) }
let_it_be(:manifest_two) { create(:dependency_proxy_manifest) }
let_it_be(:manifests) { [manifest_one, manifest_two] }
let_it_be(:ids) { manifests.map(&:id) }
it 'order_id_desc' do
expect(described_class.where(id: ids).order_id_desc.to_a).to eq [manifest_two, manifest_one]
end
end
describe 'file is being stored' do
subject { create(:dependency_proxy_manifest) }

View File

@ -306,7 +306,7 @@ RSpec.describe Issue do
end
describe '#reopen' do
let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
let_it_be_with_reload(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
it 'sets closed_at to nil when an issue is reopened' do
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
@ -316,6 +316,22 @@ RSpec.describe Issue do
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
end
it 'clears moved_to_id for moved issues' do
moved_issue = create(:issue)
issue.update!(moved_to_id: moved_issue.id)
expect { issue.reopen }.to change { issue.moved_to_id }.from(moved_issue.id).to(nil)
end
it 'clears duplicated_to_id for duplicated issues' do
duplicate_issue = create(:issue)
issue.update!(duplicated_to_id: duplicate_issue.id)
expect { issue.reopen }.to change { issue.duplicated_to_id }.from(duplicate_issue.id).to(nil)
end
it 'changes the state to opened' do
expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
end

View File

@ -116,4 +116,26 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
expect(dependency_proxy_image_count_response).to eq(manifests.size)
end
describe 'sorting and pagination' do
let(:data_path) { ['group', :dependencyProxyManifests] }
let(:current_user) { owner }
context 'with default sorting' do
let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} }
it_behaves_like 'sorted paginated query' do
let(:sort_param) { '' }
let(:first_param) { 2 }
let(:all_records) { descending_manifests }
end
end
def pagination_query(params)
# remove sort since the type does not accept sorting, but be future proof
graphql_query_for('group', { 'fullPath' => group.full_path },
query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil))
)
end
end
end

View File

@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:token) { Digest::SHA256.hexdigest('123') }
let(:headers) do
{
'docker-content-digest' => dependency_proxy_manifest.digest,
DependencyProxy::Manifest::DIGEST_HEADER => dependency_proxy_manifest.digest,
'content-type' => dependency_proxy_manifest.content_type
}
end
@ -100,8 +100,8 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:content_type) { 'new-content-type' }
before do
stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
stub_manifest_head(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
stub_manifest_download(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
end
it_behaves_like 'returning no manifest'

View File

@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do
let(:content_type) { 'foo' }
let(:headers) do
{
'docker-content-digest' => digest,
DependencyProxy::Manifest::DIGEST_HEADER => digest,
'content-type' => content_type
}
end

View File

@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do
let(:digest) { '12345' }
let(:content_type) { 'foo' }
let(:headers) do
{ 'docker-content-digest' => digest, 'content-type' => content_type }
{ DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }
end
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }

View File

@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d
end
end
def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' })
def stub_put_manifest_request(tag, status = 200, headers = { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '', headers: headers)
end
def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest })
.to_return(status: 200, body: '', headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
end
def stub_digest_config(digest, created_at)

View File

@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")

View File

@ -9,8 +9,8 @@ RSpec.describe 'groups/settings/_remove.html.haml' do
render 'groups/settings/remove', group: group
expect(rendered).to have_selector '[data-testid="remove-group-button"]'
expect(rendered).not_to have_selector '[data-testid="remove-group-button"].disabled'
expect(rendered).to have_selector '[data-button-testid="remove-group-button"]'
expect(rendered).not_to have_selector '[data-button-testid="remove-group-button"].disabled'
expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]'
end
end

View File

@ -1,22 +0,0 @@
#!/bin/sh
root="$(cd "$(dirname "$0")/../.." || exit ; pwd -P)"
if [ $# -ne 0 ]; then
shellcheck --exclude=SC1071 --external-sources "$@"
else
find \
"${root}/bin" \
"${root}/tooling" \
-type f \
-not -path "*.swp" \
-not -path "*.rb" \
-not -path "*.js" \
-not -path "*.md" \
-not -path "*.haml" \
-not -path "*/Gemfile*" \
-not -path '*/.bundle*' \
-not -path '*/Makefile*' \
-print0 \
| xargs -0 shellcheck --exclude=SC1071 --external-sources --
fi