Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b808458daa
commit
e6a54b33a9
|
@ -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:
|
||||
|
|
|
@ -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 #
|
||||
#######################
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
mutation mergeRequestAttentionRequired($projectPath: ID!, $iid: String!, $userId: ID!) {
|
||||
mergeRequestAttentionRequired(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}\""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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) }
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
10
bin/web
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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/).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}\"")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue