diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-01 18:12:07 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-01 18:12:07 +0300 |
commit | 7b197a72aac71c97cf234401a585aba58841ed64 (patch) | |
tree | 29212c9616269c257430a8cb5c247e45cbf17c7c | |
parent | a53033814ddff597cd05244f378915bacdcb5aea (diff) |
Add latest changes from gitlab-org/gitlab@master
58 files changed, 979 insertions, 672 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 6fc381fc7c2..f86b2d917f4 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -336,7 +336,7 @@ .qa-patterns: &qa-patterns - ".dockerignore" - - "qa/**/*" + - "{,jh/}qa/**/*" # Code patterns + .ci-patterns .code-patterns: &code-patterns @@ -414,7 +414,7 @@ - ".gitlab/ci/**/*" # QA changes - ".dockerignore" - - "qa/**/*" + - "{,jh/}qa/**/*" # Mapped patterns (see tests.yml) - "data/whats_new/*.yml" @@ -448,7 +448,7 @@ - "{,spec/}tooling/**/*" # QA changes - ".dockerignore" - - "qa/**/*" + - "{,jh/}qa/**/*" # Mapped patterns (see tests.yml) - "data/whats_new/*.yml" @@ -483,7 +483,7 @@ - "{,spec/}tooling/**/*" # QA changes - ".dockerignore" - - "qa/**/*" + - "{,jh/}qa/**/*" # Workhorse changes - "GITLAB_WORKHORSE_VERSION" - "workhorse/**/*" diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue index 632c2ed371f..0daf77e03dc 100644 --- a/app/assets/javascripts/issues/show/components/app.vue +++ b/app/assets/javascripts/issues/show/components/app.vue @@ -51,20 +51,11 @@ export default { required: true, type: Boolean, }, - canDestroy: { - required: true, - type: Boolean, - }, showInlineEditButton: { type: Boolean, required: false, default: true, }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, enableAutocomplete: { type: Boolean, required: false, @@ -494,14 +485,12 @@ export default { :endpoint="endpoint" :form-state="formState" :initial-description-text="initialDescriptionText" - :can-destroy="canDestroy" :issuable-templates="issuableTemplates" :markdown-docs-path="markdownDocsPath" :markdown-preview-path="markdownPreviewPath" :project-path="projectPath" :project-id="projectId" :project-namespace="projectNamespace" - :show-delete-button="showDeleteButton" :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" :issuable-type="issuableType" diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue index 47b09bd6aa0..f86ee11e64b 100644 --- a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue +++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue @@ -13,7 +13,8 @@ export default { props: { issuePath: { type: String, - required: true, + required: false, + default: '', }, issueType: { type: String, diff --git a/app/assets/javascripts/issues/show/components/edit_actions.vue b/app/assets/javascripts/issues/show/components/edit_actions.vue index 358b53bd131..120034b8d67 100644 --- a/app/assets/javascripts/issues/show/components/edit_actions.vue +++ b/app/assets/javascripts/issues/show/components/edit_actions.vue @@ -1,12 +1,10 @@ <script> -import { GlButton, GlModalDirective } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; -import { __, sprintf } from '~/locale'; +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; import Tracking from '~/tracking'; import eventHub from '../event_hub'; import updateMixin from '../mixins/update'; import getIssueStateQuery from '../queries/get_issue_state.query.graphql'; -import DeleteIssueModal from './delete_issue_modal.vue'; const issuableTypes = { issue: __('Issue'), @@ -18,18 +16,10 @@ const trackingMixin = Tracking.mixin({ label: 'delete_issue' }); export default { components: { - DeleteIssueModal, GlButton, }, - directives: { - GlModal: GlModalDirective, - }, mixins: [trackingMixin, updateMixin], props: { - canDestroy: { - type: Boolean, - required: true, - }, endpoint: { required: true, type: String, @@ -38,11 +28,6 @@ export default { type: Object, required: true, }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, issuableType: { type: String, required: true, @@ -53,7 +38,6 @@ export default { deleteLoading: false, skipApollo: false, issueState: {}, - modalId: uniqueId('delete-issuable-modal-'), }; }, apollo: { @@ -68,17 +52,9 @@ export default { }, }, computed: { - deleteIssuableButtonText() { - return sprintf(__('Delete %{issuableType}'), { - issuableType: this.typeToShow.toLowerCase(), - }); - }, isSubmitEnabled() { return this.formState.title.trim() !== ''; }, - shouldShowDeleteButton() { - return this.canDestroy && this.showDeleteButton && this.typeToShow; - }, typeToShow() { const { issueState, issuableType } = this; const type = issueState.issueType ?? issuableType; @@ -89,52 +65,26 @@ export default { closeForm() { eventHub.$emit('close.form'); }, - deleteIssuable() { - this.deleteLoading = true; - eventHub.$emit('delete.issuable'); - }, }, }; </script> <template> - <div class="gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between"> - <div> - <gl-button - :loading="formState.updateLoading" - :disabled="formState.updateLoading || !isSubmitEnabled" - category="primary" - variant="confirm" - class="gl-mr-3" - data-testid="issuable-save-button" - type="submit" - @click.prevent="updateIssuable" - > - {{ __('Save changes') }} - </gl-button> - <gl-button data-testid="issuable-cancel-button" @click="closeForm"> - {{ __('Cancel') }} - </gl-button> - </div> - <div v-if="shouldShowDeleteButton"> - <gl-button - v-gl-modal="modalId" - :loading="deleteLoading" - :disabled="deleteLoading" - category="secondary" - variant="danger" - data-testid="issuable-delete-button" - @click="track('click_button')" - > - {{ deleteIssuableButtonText }} - </gl-button> - <delete-issue-modal - :issue-path="endpoint" - :issue-type="typeToShow" - :modal-id="modalId" - :title="deleteIssuableButtonText" - @delete="deleteIssuable" - /> - </div> + <div class="gl-mt-3 gl-mb-3 gl-display-flex"> + <gl-button + :loading="formState.updateLoading" + :disabled="formState.updateLoading || !isSubmitEnabled" + category="primary" + variant="confirm" + class="gl-mr-3" + data-testid="issuable-save-button" + type="submit" + @click.prevent="updateIssuable" + > + {{ __('Save changes') }} + </gl-button> + <gl-button data-testid="issuable-cancel-button" @click="closeForm"> + {{ __('Cancel') }} + </gl-button> </div> </template> diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue index e2c12edf46d..f479c8ae78d 100644 --- a/app/assets/javascripts/issues/show/components/form.vue +++ b/app/assets/javascripts/issues/show/components/form.vue @@ -22,10 +22,6 @@ export default { LockedWarning, }, props: { - canDestroy: { - type: Boolean, - required: true, - }, endpoint: { type: String, required: true, @@ -63,11 +59,6 @@ export default { type: String, required: true, }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, canAttachFile: { type: Boolean, required: false, @@ -231,12 +222,6 @@ export default { :enable-autocomplete="enableAutocomplete" /> - <edit-actions - :endpoint="endpoint" - :form-state="formState" - :can-destroy="canDestroy" - :show-delete-button="showDeleteButton" - :issuable-type="issuableType" - /> + <edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" /> </form> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index fea94a0f7c7..fe69e96bd87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -168,7 +168,7 @@ export default { </p> </template> <template v-else-if="!hasPipeline"> - <gl-loading-icon size="lg" /> + <gl-loading-icon size="md" /> <p class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0" data-testid="monitoring-pipeline-message" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue index 86caccfc95b..2bba8d2dc82 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -1,13 +1,23 @@ <script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; import StatusIcon from './mr_widget_status_icon.vue'; import Actions from './action_buttons.vue'; export default { components: { + GlButton, StatusIcon, Actions, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { + mr: { + type: Object, + required: true, + }, isLoading: { type: Boolean, required: false, @@ -24,6 +34,10 @@ export default { default: () => [], }, }, + i18n: { + expandDetailsTooltip: __('Expand merge details'), + collapseDetailsTooltip: __('Collapse merge details'), + }, }; </script> @@ -36,18 +50,37 @@ export default { <slot name="icon"> <status-icon :status="status" /> </slot> - <div - :class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }" - class="media-body" - > - <slot></slot> + <div class="gl-display-flex gl-w-full"> + <div + :class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }" + class="media-body" + > + <slot></slot> + <div + :class="{ 'gl-flex-direction-column-reverse': !actions.length }" + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + > + <slot name="actions"> + <actions v-if="actions.length" :tertiary-buttons="actions" /> + </slot> + </div> + </div> <div - :class="{ 'gl-flex-direction-column-reverse': !actions.length }" - class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1" > - <slot name="actions"> - <actions v-if="actions.length" :tertiary-buttons="actions" /> - </slot> + <gl-button + v-gl-tooltip + :title=" + mr.mergeDetailsCollapsed + ? $options.i18n.expandDetailsTooltip + : $options.i18n.collapseDetailsTooltip + " + :icon="mr.mergeDetailsCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'" + category="tertiary" + size="small" + class="gl-vertical-align-top" + @click="() => mr.toggleMergeDetails()" + /> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue index f97c980ea36..79e878431ed 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -6,11 +6,17 @@ export default { components: { StateContainer, }, + props: { + mr: { + type: Object, + required: true, + }, + }, }; </script> <template> - <state-container status="failed"> + <state-container :mr="mr" status="failed"> <span class="gl-font-weight-bold"> {{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }} </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 9a311b3c8f4..3c6c2a44e70 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -150,7 +150,7 @@ export default { }; </script> <template> - <state-container status="scheduled" :is-loading="loading" :actions="actions"> + <state-container :mr="mr" status="scheduled" :is-loading="loading" :actions="actions"> <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="3" width="24" height="24" rx="4" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 97a9aba970f..39c56cbb93d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -58,7 +58,7 @@ export default { }; </script> <template> - <state-container status="failed" :actions="actions"> + <state-container :mr="mr" status="failed" :actions="actions"> <span class="gl-font-weight-bold"> <template v-if="mergeError">{{ mergeError }}</template> {{ s__('mrWidget|This merge request failed to be merged automatically') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index 3c63a724e15..922075516f3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -6,10 +6,16 @@ export default { components: { StateContainer, }, + props: { + mr: { + type: Object, + required: true, + }, + }, }; </script> <template> - <state-container status="loading"> + <state-container :mr="mr" status="loading"> <span class="gl-font-weight-bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index c15c6f253c2..d60d3cfc9ea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -86,7 +86,7 @@ export default { }; </script> <template> - <state-container status="failed" :is-loading="isLoading"> + <state-container :mr="mr" status="failed" :is-loading="isLoading"> <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="7" width="150" height="16" rx="4" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index dea4b2e83c0..8a7f15d8d1a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -95,12 +95,12 @@ export default { }; </script> <template> - <state-container v-if="isRefreshing" status="loading"> + <state-container v-if="isRefreshing" :mr="mr" status="loading"> <span class="gl-font-weight-bold"> {{ s__('mrWidget|Refreshing now') }} </span> </state-container> - <state-container v-else status="failed" :actions="actions"> + <state-container v-else :mr="mr" status="failed" :actions="actions"> <span class="gl-font-weight-bold"> <span v-if="mr.mergeError" class="has-error-message" data-testid="merge-error"> {{ mergeError }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index dc86aa17c65..e9298b0c856 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -150,7 +150,7 @@ export default { }; </script> <template> - <state-container :actions="actions" status="merged"> + <state-container :mr="mr" :actions="actions" status="merged"> <mr-widget-author-time :action-text="s__('mrWidget|Merged by')" :author="mr.metrics.mergedBy" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 781612135c2..37c8d5d15f3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -152,7 +152,7 @@ export default { }; </script> <template> - <state-container :status="status" :is-loading="isLoading"> + <state-container :mr="mr" :status="status" :is-loading="isLoading"> <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="3" width="24" height="24" rx="4" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 000ea2d0bdc..27919f90cc3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -22,7 +22,7 @@ export default { </script> <template> - <state-container status="failed"> + <state-container :mr="mr" status="failed"> <span class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!" data-qa-selector="head_mismatch_content" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 43be8444cb5..8f2e4eb2131 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -24,7 +24,7 @@ export default { </script> <template> - <state-container status="failed"> + <state-container :mr="mr" status="failed"> <span class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 43d26d34a93..0458e9dfaf5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -163,7 +163,7 @@ export default { </script> <template> - <state-container status="failed"> + <state-container :mr="mr" status="failed"> <span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1"> {{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }} </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 77d6c51a3dc..c8a2a8d119b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -230,6 +230,11 @@ export default { shouldShowCodeQualityExtension() { return window.gon?.features?.refactorCodeQualityExtension; }, + shouldShowMergeDetails() { + if (this.mr.state === 'readyToMerge') return true; + + return !this.mr.mergeDetailsCollapsed; + }, }, watch: { 'mr.machineValue': { @@ -318,6 +323,12 @@ export default { this.initPolling(); this.bindEventHubListeners(); eventHub.$on('mr.discussion.updated', this.checkStatus); + + window.addEventListener('resize', () => { + if (window.innerWidth >= 768) { + this.mr.toggleMergeDetails(false); + } + }); }, getServiceEndpoints(store) { return { @@ -621,7 +632,12 @@ export default { <div class="mr-widget-section" data-qa-selector="mr_widget_content"> <component :is="componentName" :mr="mr" :service="service" /> - <ready-to-merge v-if="mr.commitsCount" :mr="mr" :service="service" /> + <ready-to-merge + v-if="mr.commitsCount" + v-show="shouldShowMergeDetails" + :mr="mr" + :service="service" + /> </div> </div> <mr-widget-pipeline-container diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index ae2f95f4cfa..e6ff586892f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -28,6 +28,7 @@ export default class MergeRequestStore { this.stateMachine = machine(STATE_MACHINE.definition); this.machineValue = this.stateMachine.value; + this.mergeDetailsCollapsed = window.innerWidth < 768; this.setPaths(data); @@ -405,4 +406,8 @@ export default class MergeRequestStore { this.transitionStateMachine(transitionOptions); } + + toggleMergeDetails(val = !this.mergeDetailsCollapsed) { + this.mergeDetailsCollapsed = val; + } } diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue new file mode 100644 index 00000000000..34874908f9b --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue @@ -0,0 +1,109 @@ +<script> +import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; + +import { __ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; + +import { STATE_OPEN } from '../../constants'; +import WorkItemLinksMenu from './work_item_links_menu.vue'; + +export default { + components: { + GlButton, + GlIcon, + RichTimestampTooltip, + WorkItemLinksMenu, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + projectPath: { + type: String, + required: true, + }, + canUpdate: { + type: Boolean, + required: true, + }, + issuableGid: { + type: String, + required: true, + }, + childItem: { + type: Object, + required: true, + }, + }, + computed: { + isItemOpen() { + return this.childItem.state === STATE_OPEN; + }, + iconClass() { + return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500'; + }, + iconName() { + return this.isItemOpen ? 'issue-open-m' : 'issue-close'; + }, + stateTimestamp() { + return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt; + }, + stateTimestampTypeText() { + return this.isItemOpen ? __('Created') : __('Closed'); + }, + childPath() { + return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`; + }, + }, +}; +</script> + +<template> + <div + class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" + data-testid="links-child" + > + <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> + <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon"> + <gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" /> + </span> + <rich-timestamp-tooltip + :target="`stateIcon-${childItem.id}`" + :raw-timestamp="stateTimestamp" + :timestamp-type-text="stateTimestampTypeText" + /> + <gl-icon + v-if="childItem.confidential" + v-gl-tooltip.top + name="eye-slash" + class="gl-mr-2 gl-text-orange-500" + data-testid="confidential-icon" + :aria-label="__('Confidential')" + :title="__('Confidential')" + /> + <gl-button + :href="childPath" + category="tertiary" + variant="link" + class="gl-text-truncate gl-max-w-80 gl-text-black-normal!" + @click="$emit('click', childItem.id, $event)" + @mouseover="$emit('mouseover', childItem.id, $event)" + @mouseout="$emit('mouseout', childItem.id, $event)" + > + {{ childItem.title }} + </gl-button> + </div> + <div + v-if="canUpdate" + class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center" + > + <work-item-links-menu + :work-item-id="childItem.id" + :parent-work-item-id="issuableGid" + data-testid="links-menu" + @removeChild="$emit('remove', childItem.id)" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index b13162a81d5..99907ed7f57 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -9,18 +9,13 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g import { isMetaKey } from '~/lib/utils/common_utils'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; -import { - STATE_OPEN, - WIDGET_ICONS, - WORK_ITEM_STATUS_TEXT, - WIDGET_TYPE_HIERARCHY, -} from '../../constants'; +import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants'; import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import workItemQuery from '../../graphql/work_item.query.graphql'; import WorkItemDetailModal from '../work_item_detail_modal.vue'; +import WorkItemLinkChild from './work_item_link_child.vue'; import WorkItemLinksForm from './work_item_links_form.vue'; -import WorkItemLinksMenu from './work_item_links_menu.vue'; export default { components: { @@ -28,8 +23,8 @@ export default { GlIcon, GlAlert, GlLoadingIcon, + WorkItemLinkChild, WorkItemLinksForm, - WorkItemLinksMenu, WorkItemDetailModal, }, directives: { @@ -124,12 +119,6 @@ export default { }, }, methods: { - iconClass(state) { - return state === STATE_OPEN ? 'gl-text-green-500' : 'gl-text-blue-500'; - }, - iconName(state) { - return state === STATE_OPEN ? 'issue-open-m' : 'issue-close'; - }, toggle() { this.isOpen = !this.isOpen; }, @@ -171,9 +160,6 @@ export default { replace: true, }); }, - childPath(childItemId) { - return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(childItemId)}`; - }, toggleChildFromCache(workItem, childId, store) { const sourceData = store.readQuery({ query: getWorkItemLinksQuery, @@ -322,48 +308,18 @@ export default { @cancel="hideAddForm" @addWorkItemChild="addChild" /> - <div + <work-item-link-child v-for="child in children" :key="child.id" - class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" - data-testid="links-child" - > - <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> - <gl-icon - :name="iconName(child.state)" - class="gl-mr-3" - :class="iconClass(child.state)" - /> - <gl-icon - v-if="child.confidential" - v-gl-tooltip.top - name="eye-slash" - class="gl-mr-2 gl-text-orange-500" - data-testid="confidential-icon" - :title="__('Confidential')" - /> - <gl-button - :href="childPath(child.id)" - category="tertiary" - variant="link" - class="gl-text-truncate gl-max-w-80 gl-text-black-normal!" - @click="openChild(child.id, $event)" - @mouseover="prefetchWorkItem(child.id)" - @mouseout="clearPrefetching" - > - {{ child.title }} - </gl-button> - </div> - <div class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"> - <work-item-links-menu - v-if="canUpdate" - :work-item-id="child.id" - :parent-work-item-id="issuableGid" - data-testid="links-menu" - @removeChild="removeChild(child.id)" - /> - </div> - </div> + :project-path="projectPath" + :can-update="canUpdate" + :issuable-gid="issuableGid" + :child-item="child" + @click="openChild" + @mouseover="prefetchWorkItem" + @mouseout="clearPrefetching" + @remove="removeChild" + /> <work-item-detail-modal ref="modal" :work-item-id="activeChildId" diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql index 2ca4450f892..7b63d9c7ca3 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql @@ -26,6 +26,8 @@ query workItemLinksQuery($id: WorkItemID!) { } title state + createdAt + closedAt } } } diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 6ee719c46aa..f04412d7603 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -62,9 +62,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic options[:allow_tree_conflicts] ] - if Feature.enabled?(:etag_merge_request_diff_batches, @merge_request.project) - return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) - end + return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) if diff_options_hash[:paths].blank? if Feature.enabled?(:remove_caching_diff_batches, @merge_request.project) diff --git a/app/services/ci/queue/pending_builds_strategy.rb b/app/services/ci/queue/pending_builds_strategy.rb index c8bdbba5e65..cfafe66d10b 100644 --- a/app/services/ci/queue/pending_builds_strategy.rb +++ b/app/services/ci/queue/pending_builds_strategy.rb @@ -19,7 +19,11 @@ module Ci def builds_for_group_runner return new_builds.none if runner.namespace_ids.empty? - new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids) + new_builds_relation = new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids) + + return order(new_builds_relation) if ::Feature.enabled?(:order_builds_for_group_runner) + + new_builds_relation end def builds_matching_tag_ids(relation, ids) diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 503e7d8afa6..bacfe056683 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -15,7 +15,7 @@ = f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8 %span.form-text.text-muted = s_('OutboundRequests|Requests to these domains and IP addresses are accessible to both system hooks and web hooks even when local requests are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 and 127.0.0.0/28 are supported. Domain wildcards are not supported. To separate entries use commas, semicolons, or newlines. The allowlist can hold a maximum of 1000 entries. Domains must be IDNA encoded.') - = link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'create-an-allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer' .form-group = f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled, diff --git a/config/feature_categories.yml b/config/feature_categories.yml index 9b5f3de3f75..a8062111f26 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -13,7 +13,6 @@ - application_performance - attack_emulation - audit_events -- audit_reports - authentication_and_authorization - auto_devops - backup_restore diff --git a/config/feature_flags/development/etag_merge_request_diff_batches.yml b/config/feature_flags/development/ci_cost_factors_narrow_os_contribution_by_plan.yml index 8cd3ba8637a..0d73e8735e9 100644 --- a/config/feature_flags/development/etag_merge_request_diff_batches.yml +++ b/config/feature_flags/development/ci_cost_factors_narrow_os_contribution_by_plan.yml @@ -1,8 +1,8 @@ --- -name: etag_merge_request_diff_batches -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93953 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/369488 -milestone: '15.3' +name: ci_cost_factors_narrow_os_contribution_by_plan +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96273 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372263 +milestone: '15.4' type: development -group: group::code review +group: group::pipeline execution default_enabled: false diff --git a/config/feature_flags/development/order_builds_for_group_runner.yml b/config/feature_flags/development/order_builds_for_group_runner.yml new file mode 100644 index 00000000000..50f9a301ad6 --- /dev/null +++ b/config/feature_flags/development/order_builds_for_group_runner.yml @@ -0,0 +1,8 @@ +--- +name: order_builds_for_group_runner +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94815 +rollout_issue_url: +milestone: '15.4' +type: development +group: group::pipeline execution +default_enabled: true diff --git a/db/docs/protected_environment_deploy_access_levels.yml b/db/docs/protected_environment_deploy_access_levels.yml index cd3bba9171f..c25044dc7e2 100644 --- a/db/docs/protected_environment_deploy_access_levels.yml +++ b/db/docs/protected_environment_deploy_access_levels.yml @@ -1,7 +1,7 @@ --- table_name: protected_environment_deploy_access_levels classes: -- ProtectedEnvironment::DeployAccessLevel +- ProtectedEnvironments::DeployAccessLevel feature_categories: - continuous_delivery description: https://docs.gitlab.com/ee/ci/environments/protected_environments.html diff --git a/doc/.vale/gitlab/InternalLinkExtension.yml b/doc/.vale/gitlab/InternalLinkExtension.yml index 5783c4347a9..52142b50dfc 100644 --- a/doc/.vale/gitlab/InternalLinkExtension.yml +++ b/doc/.vale/gitlab/InternalLinkExtension.yml @@ -5,9 +5,9 @@ # # For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles extends: existence -message: 'Link "%s" must use the .md file extension.' +message: 'Link "%s" must link directly to a file and use the .md file extension.' link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links-to-internal-documentation level: error scope: raw raw: - - '\[.+\]\([\w\/\.-]+\.html[^)]*\)' + - '\[[^\]]+\]\([^:\)]+(\/(#[^\)]+)?\)|\.html(#.+)?\))' diff --git a/doc/api/group_protected_environments.md b/doc/api/group_protected_environments.md index 8a7a3ecd3bc..3f1d932e0c8 100644 --- a/doc/api/group_protected_environments.md +++ b/doc/api/group_protected_environments.md @@ -15,7 +15,7 @@ Read more about [group-level protected environments](../ci/environments/protecte ## Valid access levels -The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. +The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. Currently, these levels are recognized: ```plaintext diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md index 4c6f509a752..5b52d11feda 100644 --- a/doc/api/protected_environments.md +++ b/doc/api/protected_environments.md @@ -11,7 +11,7 @@ type: concepts, howto ## Valid access levels -The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. +The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. Currently, these levels are recognized: ```plaintext diff --git a/doc/ci/cloud_services/index.md b/doc/ci/cloud_services/index.md index b460af2d96e..0fac5ae112d 100644 --- a/doc/ci/cloud_services/index.md +++ b/doc/ci/cloud_services/index.md @@ -16,7 +16,7 @@ GitLab CI/CD supports [OpenID Connect (OIDC)](https://openid.net/connect/faq/) t - Account on GitLab. - Access to a cloud provider that supports OIDC to configure authorization and create roles. -The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault. +The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/index.md). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault. NOTE: Configuring OIDC enables JWT token access to the target environments for all pipelines. diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md index 788061b2955..90efc7ba9ef 100644 --- a/doc/ci/environments/deployment_safety.md +++ b/doc/ci/environments/deployment_safety.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Deployment safety **(FREE)** -[Deployment jobs](../jobs/#deployment-jobs) are a specific kind of CI/CD +[Deployment jobs](../jobs/index.md#deployment-jobs) are a specific kind of CI/CD job. They can be more sensitive than other jobs in a pipeline, and might need to be treated with extra care. GitLab has several features that help maintain deployment security and stability. @@ -66,7 +66,7 @@ For more information, see [Resource Group documentation](../resource_groups/inde ## Skip outdated deployment jobs The effective execution order of pipeline jobs can vary from run to run, which -could cause undesired behavior. For example, a [deployment job](../jobs/#deployment-jobs) +could cause undesired behavior. For example, a [deployment job](../jobs/index.md#deployment-jobs) in a newer pipeline could finish before a deployment job in an older pipeline. This creates a race condition where the older deployment finishes later, overwriting the "newer" deployment. diff --git a/doc/ci/testing/unit_test_reports.md b/doc/ci/testing/unit_test_reports.md index 6294996c703..28356a62c99 100644 --- a/doc/ci/testing/unit_test_reports.md +++ b/doc/ci/testing/unit_test_reports.md @@ -156,7 +156,7 @@ If parsing JUnit report XML results in an error, an indicator is shown next to t ![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png) -For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/#gitlab-cicd). +For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/index.md#gitlab-cicd). GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional. diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md index 43482134d2f..d68432e567e 100644 --- a/doc/ci/yaml/artifacts_reports.md +++ b/doc/ci/yaml/artifacts_reports.md @@ -169,7 +169,7 @@ GitLab can display the results of one or more reports in: - The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline). - The [security dashboard](../../user/application_security/security_dashboard/index.md). - The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md). -- The [dependency list](../../user/application_security/dependency_list/). +- The [dependency list](../../user/application_security/dependency_list/index.md). ## `artifacts:reports:dotenv` diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index 5b779f22bd3..e174e02546a 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -177,7 +177,7 @@ If you get this error message while configuring GitLab, the following are possib - GitLab is unable to reach your Jenkins instance at the address. If your GitLab instance is self-managed, try pinging the Jenkins instance at the domain provided on the GitLab instance. - The Jenkins instance is at a local address and is not included in the - [GitLab installation's allowlist](../security/webhooks.md#allowlist-for-local-requests). + [GitLab installation's allowlist](../security/webhooks.md#create-an-allowlist-for-local-requests). - The credentials for the Jenkins instance do not have sufficient access or are invalid. - The **Enable authentication for `/project` end-point** checkbox is not selected in your [Jenkin's plugin configuration](#configure-the-jenkins-server). diff --git a/doc/security/img/allowlist_v13_0.png b/doc/security/img/allowlist_v13_0.png Binary files differdeleted file mode 100644 index 973b53a57a4..00000000000 --- a/doc/security/img/allowlist_v13_0.png +++ /dev/null diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index dcd4c76ab65..b2973155950 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -7,78 +7,80 @@ type: concepts, reference, howto # Webhooks and insecure internal web services **(FREE SELF)** -Users with at least the Maintainer role can set up [Webhooks](../user/project/integrations/webhooks.md) that are +Users with at least the Maintainer role can set up [webhooks](../user/project/integrations/webhooks.md) that are triggered when specific changes occur in a project. When triggered, a `POST` HTTP request is sent to a URL. A webhook is -usually configured to send data to a specific external web service, which -processes the data in an appropriate way. +usually configured to send data to a specific external web service, which processes the data in an appropriate way. -However, a Webhook can be configured with a URL for an internal web service instead of an external web service. -When the Webhook is triggered, -non-GitLab web services running on your GitLab server or in its local network could be exploited. +However, a webhook can be configured with a URL for an internal web service instead of an external web service. +When the webhook is triggered, non-GitLab web services running on your GitLab server or in its local network could be +exploited. -Webhook requests are made by the GitLab server itself and use a single -(optional) secret token per hook for authorization (instead of a user or -repository-specific token). As a result, these requests may have broader access than -intended, including access to everything running on the server hosting the webhook. This -may include the GitLab server or API itself (for example, `http://localhost:123`). -Depending on the called webhook, this may also result in network access -to other servers within that webhook server's local network (for example, -`http://192.168.1.12:345`), even if these services are otherwise protected -and inaccessible from the outside world. +Webhook requests are made by the GitLab server itself and use a single optional secret token per hook for authorization +instead of: -If a web service does not require authentication, Webhooks can be used to -trigger destructive commands by getting the GitLab server to make POST requests -to endpoints like `http://localhost:123/some-resource/delete`. +- A user token. +- A repository-specific token. -## Allow requests to local network +As a result, these requests can have broader access than intended, including access to everything running on the server +that hosts the webhook including: -To prevent this type of exploitation from happening, starting with GitLab 10.6, -all Webhook requests to the current GitLab instance server address and/or in a -private network are forbidden by default. That means that all requests made -to `127.0.0.1`, `::1` and `0.0.0.0`, as well as IPv4 `10.0.0.0/8`, `172.16.0.0/12`, -`192.168.0.0/16` and IPv6 site-local (`ffc0::/10`) addresses aren't allowed. +- The GitLab server. +- The API itself. +- For some webhooks, network access to other servers in that webhook server's local network, even if these services + are otherwise protected and inaccessible from the outside world. -This behavior can be overridden: +Webhooks can be used to trigger destructive commands using web services that don't require authentication. These webhooks +can get the GitLab server to make `POST` HTTP requests to endpoints that delete resources. + +## Allow webhook and service requests to local network + +To prevent exploitation of insecure internal web services, all webhook requests to the following local network addresses are not allowed: + +- The current GitLab instance server address. +- Private network addresses, including `127.0.0.1`, `::1`, `0.0.0.0`, `10.0.0.0/8`, `172.16.0.0/12`, + `192.168.0.0/16`, and IPv6 site-local (`ffc0::/10`) addresses. + +To allow access to these addresses: 1. On the top bar, select **Menu > Admin**. 1. On the left sidebar, select **Settings > Network**. -1. Expand the **Outbound requests** section. -1. Select **Allow requests to the local network from web hooks and services**. +1. Expand **Outbound requests**. +1. Select the **Allow requests to the local network from web hooks and services** checkbox. -NOTE: -*System hooks* are enabled to make requests to local network by default since they are -set up by administrators. However, you can turn this off by disabling the -**Allow requests to the local network from system hooks** option. +## Prevent system hook requests to local network -## Allowlist for local requests +[System hooks](../administration/system_hooks.md) are permitted to make requests to local network by default because +they are set up by administrators. To prevent system hook requests to the local network: -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44496) in GitLab 12.2 +1. On the top bar, select **Menu > Admin**. +1. On the left sidebar, select **Settings > Network**. +1. Expand **Outbound requests**. +1. Clear the **Allow requests to the local network from system hooks** checkbox. -You can allow certain domains and IP addresses to be accessible to both *system hooks* -and *webhooks* even when local requests are not allowed by adding them to the -allowlist: +## Create an allowlist for local requests -1. On the top bar, select **Menu > Admin**. -1. On the left sidebar, select **Settings > Network** (`/admin/application_settings/network`) - and expand **Outbound requests**: +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44496) in GitLab 12.2 - ![Outbound local requests allowlist](img/allowlist_v13_0.png) +You can allow certain domains and IP addresses to be accessible to both system hooks and webhooks, even when local +requests are forbidden. To add these domains to the allowlist: -The allowed entries can be separated by semicolons, commas or whitespaces -(including newlines) and be in different formats like hostnames, IP addresses and/or -IP ranges. IPv6 is supported. Hostnames that contain Unicode characters should -use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en) -(IDNA) encoding. +1. On the top bar, select **Menu > Admin**. +1. On the left sidebar, select **Settings > Network**. +1. Expand **Outbound requests** and add entries. -The allowlist can hold a maximum of 1000 entries. Each entry can be a maximum of -255 characters. +The entries can: -You can allow a particular port by specifying it in the allowlist entry. -For example `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`. -If no port is mentioned, all ports on that IP/domain are allowed. An IP range -allows all ports on all IPs in that range. +- Be separated by semicolons, commas, or whitespaces (including newlines). +- Be in different formats like hostnames, IP addresses, IP address ranges. IPv6 is supported. Hostnames that contain + Unicode characters should use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en) + (IDNA) encoding. +- Include ports. For example, `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`. If no port is specified, + all ports on that IP address or domain are allowed. An IP address range allows all ports on all IP addresses in that + range. +- Number no more than 1000 entries of no more than 255 characters for each entry. +- Not contain wildcards (for example, `*.example.com`). -Example: +For example: ```plaintext example.com;gitlab.example.com @@ -89,9 +91,6 @@ example.com;gitlab.example.com example.com:8080 ``` -NOTE: -Wildcards (`*.example.com`) are not currently supported. - <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 3dcf9bed6c7..b75b8493d7a 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -26,7 +26,7 @@ If you are importing from GitHub Enterprise to a self-managed GitLab instance: - You must first enable [GitHub integration](../../../integration/github.md). - To import projects from GitHub Enterprise to GitLab.com, use the [Import API](../../../api/import.md). -- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#allowlist-for-local-requests) +- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests) with `github.com` and `api.github.com` to solve the hostname. For more information, read the issue [Importing a GitHub project requires DNS resolution even when behind a proxy](https://gitlab.com/gitlab-org/gitlab/-/issues/37941). diff --git a/lib/api/rpm_project_packages.rb b/lib/api/rpm_project_packages.rb index a257561df59..06b53134f35 100644 --- a/lib/api/rpm_project_packages.rb +++ b/lib/api/rpm_project_packages.rb @@ -2,11 +2,22 @@ module API class RpmProjectPackages < ::API::Base helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::Packages::BasicAuthHelpers + include ::API::Helpers::Authentication + feature_category :package_registry before do require_packages_enabled! - not_found! unless Feature.enabled?(:rpm_packages) + + not_found! unless ::Feature.enabled?(:rpm_packages, authorized_user_project) + + authorize_read_package!(authorized_user_project) + end + + authenticate_with do |accept| + accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username) + .sent_through(:http_basic_auth) end params do @@ -18,7 +29,7 @@ module API params do requires :file_name, type: String, desc: 'Repository metadata file name' end - get 'repodata/*file_name' do + get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do not_found! end @@ -27,12 +38,13 @@ module API requires :package_file_id, type: Integer, desc: 'RPM package file id' requires :file_name, type: String, desc: 'RPM package file name' end - get '*package_file_id/*file_name' do + get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do not_found! end desc 'Upload a RPM package' post do + authorize_create_package!(authorized_user_project) not_found! end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e46cf759410..5816584e38f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9391,6 +9391,9 @@ msgstr "" msgid "Collapse jobs" msgstr "" +msgid "Collapse merge details" +msgstr "" + msgid "Collapse milestones" msgstr "" @@ -12502,6 +12505,9 @@ msgstr "" msgid "Delete deploy key" msgstr "" +msgid "Delete epic" +msgstr "" + msgid "Delete file" msgstr "" @@ -14986,6 +14992,9 @@ msgstr "" msgid "Epic Boards" msgstr "" +msgid "Epic actions" +msgstr "" + msgid "Epic cannot be found." msgstr "" @@ -15687,6 +15696,9 @@ msgstr "" msgid "Expand jobs" msgstr "" +msgid "Expand merge details" +msgstr "" + msgid "Expand milestones" msgstr "" diff --git a/package.json b/package.json index 051efa44b4a..d8c5eb4749e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "jest": "jest --config jest.config.js", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", - "jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_MATCHING_TESTS_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", + "jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", "jest:integration": "jest --config jest.config.integration.js", "lint:eslint": "node scripts/frontend/eslint.js", "lint:eslint:fix": "node scripts/frontend/eslint.js --fix", diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index d58bf1be812..11c43ea4388 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; import IssuableEditActions from '~/issues/show/components/edit_actions.vue'; -import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue'; import eventHub from '~/issues/show/event_hub'; -import { - getIssueStateQueryResponse, - updateIssueStateQueryResponse, -} from '../mock_data/apollo_mock'; describe('Edit Actions component', () => { let wrapper; @@ -31,8 +24,6 @@ describe('Edit Actions component', () => { }, }; - const modalId = 'delete-issuable-modal-1'; - const createComponent = ({ props, data } = {}) => { fakeApollo = createMockApollo([], mockResolvers); @@ -50,16 +41,13 @@ describe('Edit Actions component', () => { data() { return { issueState: {}, - modalId, ...data, }; }, }); }; - const findModal = () => wrapper.findComponent(DeleteIssueModal); const findEditButtons = () => wrapper.findAllComponents(GlButton); - const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button'); const findSaveButton = () => wrapper.findByTestId('issuable-save-button'); const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button'); @@ -79,23 +67,12 @@ describe('Edit Actions component', () => { }); }); - it('does not render the delete button if canDestroy is false', () => { - createComponent({ props: { canDestroy: false } }); - expect(findDeleteButton().exists()).toBe(false); - }); - it('disables save button when title is blank', () => { createComponent({ props: { formState: { title: '', issue_type: '' } } }); expect(findSaveButton().attributes('disabled')).toBe('true'); }); - it('does not render the delete button if showDeleteButton is false', () => { - createComponent({ props: { showDeleteButton: false } }); - - expect(findDeleteButton().exists()).toBe(false); - }); - describe('updateIssuable', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); @@ -119,63 +96,4 @@ describe('Edit Actions component', () => { expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); }); - - describe('delete issue button', () => { - let trackingSpy; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it('tracks clicking on button', () => { - findDeleteButton().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'delete_issue', - }); - }); - }); - - describe('delete issue modal', () => { - it('renders', () => { - expect(findModal().props()).toEqual({ - issuePath: 'gitlab-org/gitlab-test/-/issues/1', - issueType: 'Issue', - modalId, - title: 'Delete issue', - }); - }); - }); - - describe('deleteIssuable', () => { - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - it('does not send the `delete.issuable` event when clicking delete button', () => { - findDeleteButton().vm.$emit('click'); - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - - it('sends the `delete.issuable` event when clicking the delete confirm button', async () => { - expect(eventHub.$emit).toHaveBeenCalledTimes(0); - findModal().vm.$emit('delete'); - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable'); - expect(eventHub.$emit).toHaveBeenCalledTimes(1); - }); - }); - - describe('with Apollo cache mock', () => { - it('renders the right delete button text per apollo cache type', async () => { - mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - - it('should not change the delete button text per apollo cache mutation', async () => { - mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap index 413ac0d6131..635ef0f6b0d 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap @@ -41,105 +41,134 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have </div> <div - class="media-body gl-display-flex" + class="gl-display-flex gl-w-full" > - - <h4 - class="gl-mr-3" - data-testid="statusText" - > - Set by - <a - class="author-link inline" - > - <img - class="avatar avatar-inline s16" - src="no_avatar.png" - /> - - <span - class="author" - > - - </span> - </a> - to be merged automatically when the pipeline succeeds - </h4> - <div - class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + class="media-body gl-display-flex" > - <div> - <div - class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - lazy="" - no-caret="" + + <h4 + class="gl-mr-3" + data-testid="statusText" + > + Set by + <a + class="author-link inline" > - <!----> + <img + class="avatar avatar-inline s16" + src="no_avatar.png" + /> + + <span + class="author" + > + + </span> + </a> + to be merged automatically when the pipeline succeeds + </h4> + + <div + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + > + <div> + <div + class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" + lazy="" + no-caret="" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="dropdown-icon gl-icon s16" + data-testid="ellipsis_v-icon" + role="img" + > + <use + href="#ellipsis_v" + /> + </svg> + + <span + class="gl-new-dropdown-button-text gl-sr-only" + > + + </span> + + <svg + aria-hidden="true" + class="gl-button-icon dropdown-chevron gl-icon s16" + data-testid="chevron-down-icon" + role="img" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu dropdown-menu-right" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + <button - aria-expanded="false" - aria-haspopup="true" - class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" + data-qa-selector="cancel_auto_merge_button" + data-testid="cancelAutomaticMergeButton" type="button" > <!----> - <svg - aria-hidden="true" - class="dropdown-icon gl-icon s16" - data-testid="ellipsis_v-icon" - role="img" - > - <use - href="#ellipsis_v" - /> - </svg> - + <!----> + <span - class="gl-new-dropdown-button-text gl-sr-only" + class="gl-button-text" > + Cancel auto-merge + </span> - - <svg - aria-hidden="true" - class="gl-button-icon dropdown-chevron gl-icon s16" - data-testid="chevron-down-icon" - role="img" - > - <use - href="#chevron-down" - /> - </svg> </button> - <ul - class="dropdown-menu dropdown-menu-right" - role="menu" - tabindex="-1" - > - <!----> - </ul> </div> + </div> + </div> + + <div + class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1" + > + <button + class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon" + title="Collapse merge details" + type="button" + > + <!----> - <button - class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - type="button" + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="chevron-lg-up-icon" + role="img" > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Cancel auto-merge - - </span> - </button> - </div> + <use + href="#chevron-lg-up" + /> + </svg> + + <!----> + </button> </div> </div> </div> @@ -186,105 +215,134 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c </div> <div - class="media-body gl-display-flex" + class="gl-display-flex gl-w-full" > - - <h4 - class="gl-mr-3" - data-testid="statusText" - > - Set by - <a - class="author-link inline" - > - <img - class="avatar avatar-inline s16" - src="no_avatar.png" - /> - - <span - class="author" - > - - </span> - </a> - to be merged automatically when the pipeline succeeds - </h4> - <div - class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + class="media-body gl-display-flex" > - <div> - <div - class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - lazy="" - no-caret="" + + <h4 + class="gl-mr-3" + data-testid="statusText" + > + Set by + <a + class="author-link inline" > - <!----> + <img + class="avatar avatar-inline s16" + src="no_avatar.png" + /> + + <span + class="author" + > + + </span> + </a> + to be merged automatically when the pipeline succeeds + </h4> + + <div + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" + > + <div> + <div + class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" + lazy="" + no-caret="" + > + <!----> + <button + aria-expanded="false" + aria-haspopup="true" + class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="dropdown-icon gl-icon s16" + data-testid="ellipsis_v-icon" + role="img" + > + <use + href="#ellipsis_v" + /> + </svg> + + <span + class="gl-new-dropdown-button-text gl-sr-only" + > + + </span> + + <svg + aria-hidden="true" + class="gl-button-icon dropdown-chevron gl-icon s16" + data-testid="chevron-down-icon" + role="img" + > + <use + href="#chevron-down" + /> + </svg> + </button> + <ul + class="dropdown-menu dropdown-menu-right" + role="menu" + tabindex="-1" + > + <!----> + </ul> + </div> + <button - aria-expanded="false" - aria-haspopup="true" - class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" + class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" + data-qa-selector="cancel_auto_merge_button" + data-testid="cancelAutomaticMergeButton" type="button" > <!----> - <svg - aria-hidden="true" - class="dropdown-icon gl-icon s16" - data-testid="ellipsis_v-icon" - role="img" - > - <use - href="#ellipsis_v" - /> - </svg> - + <!----> + <span - class="gl-new-dropdown-button-text gl-sr-only" + class="gl-button-text" > + Cancel auto-merge + </span> - - <svg - aria-hidden="true" - class="gl-button-icon dropdown-chevron gl-icon s16" - data-testid="chevron-down-icon" - role="img" - > - <use - href="#chevron-down" - /> - </svg> </button> - <ul - class="dropdown-menu dropdown-menu-right" - role="menu" - tabindex="-1" - > - <!----> - </ul> </div> + </div> + </div> + + <div + class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1" + > + <button + class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon" + title="Collapse merge details" + type="button" + > + <!----> - <button - class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - type="button" + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="chevron-lg-up-icon" + role="img" > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Cancel auto-merge - - </span> - </button> - </div> + <use + href="#chevron-lg-up" + /> + </svg> + + <!----> + </button> </div> </div> </div> diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js index 5f2555b7a77..5c07f4ce143 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js @@ -6,7 +6,7 @@ describe('MRWidgetArchived', () => { let wrapper; beforeEach(() => { - wrapper = shallowMount(archivedComponent); + wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } }); }); afterEach(() => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js index 4021aff6014..ac18ccf9e26 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js @@ -6,7 +6,7 @@ describe('MRWidgetChecking', () => { let wrapper; beforeEach(() => { - wrapper = shallowMount(CheckingComponent); + wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } }); }); afterEach(() => { diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js new file mode 100644 index 00000000000..1d5472a0473 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -0,0 +1,122 @@ +import { GlButton, GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue'; + +import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; +import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; + +import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data'; + +describe('WorkItemLinkChild', () => { + const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2'; + let wrapper; + + const createComponent = ({ + projectPath = 'gitlab-org/gitlab-test', + canUpdate = true, + issuableGid = WORK_ITEM_ID, + childItem = workItemTask, + } = {}) => { + wrapper = shallowMountExtended(WorkItemLinkChild, { + propsData: { + projectPath, + canUpdate, + issuableGid, + childItem, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents + ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'} + ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'} + `( + 'renders item status icon and tooltip when item status is `$status`', + ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => { + createComponent({ childItem }); + + const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon); + const statusTooltip = wrapper.findComponent(RichTimestampTooltip); + + expect(statusIcon.props('name')).toBe(statusIconName); + expect(statusIcon.classes()).toContain(statusIconColorClass); + expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp); + expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents); + }, + ); + + it('renders confidential icon when item is confidential', () => { + createComponent({ childItem: confidentialWorkItemTask }); + + const confidentialIcon = wrapper.findByTestId('confidential-icon'); + + expect(confidentialIcon.props('name')).toBe('eye-slash'); + expect(confidentialIcon.attributes('title')).toBe('Confidential'); + }); + + describe('item title', () => { + let titleEl; + + beforeEach(() => { + createComponent(); + + titleEl = wrapper.findComponent(GlButton); + }); + + it('renders item title', () => { + expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4'); + expect(titleEl.text()).toBe(workItemTask.title); + }); + + it.each` + action | event | emittedEvent + ${'clicking'} | ${'click'} | ${'click'} + ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'} + ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'} + `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => { + const eventObj = { + preventDefault: jest.fn(), + }; + titleEl.vm.$emit(event, eventObj); + + expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]); + }); + }); + + describe('item menu', () => { + let itemMenuEl; + + beforeEach(() => { + createComponent(); + + itemMenuEl = wrapper.findComponent(WorkItemLinksMenu); + }); + + it('renders work-item-links-menu', () => { + expect(itemMenuEl.exists()).toBe(true); + + expect(itemMenuEl.attributes()).toMatchObject({ + 'work-item-id': workItemTask.id, + 'parent-work-item-id': WORK_ITEM_ID, + }); + }); + + it('does not render work-item-links-menu when canUpdate is false', () => { + createComponent({ canUpdate: false }); + + expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false); + }); + + it('removeChild event on menu triggers `click-remove-child` event', () => { + itemMenuEl.vm.$emit('removeChild'); + + expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index a5deeb827f6..876aedff08b 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -1,5 +1,5 @@ import Vue, { nextTick } from 'vue'; -import { GlButton, GlIcon, GlAlert } from '@gitlab/ui'; +import { GlAlert } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; +import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; @@ -92,10 +93,10 @@ describe('WorkItemLinks', () => { const findLinksBody = () => wrapper.findByTestId('links-body'); const findEmptyState = () => wrapper.findByTestId('links-empty'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); + const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0); const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); - const findFirstLinksMenu = () => wrapper.findByTestId('links-menu'); const findChildrenCount = () => wrapper.findByTestId('children-count'); - const findChildren = () => wrapper.findAllByTestId('links-child'); beforeEach(async () => { await createComponent(); @@ -148,8 +149,7 @@ describe('WorkItemLinks', () => { it('renders all hierarchy widget children', () => { expect(findLinksBody().exists()).toBe(true); - expect(findChildren()).toHaveLength(4); - expect(findFirstLinksMenu().exists()).toBe(true); + expect(findWorkItemLinkChildItems()).toHaveLength(4); }); it('shows alert when list loading fails', async () => { @@ -164,19 +164,6 @@ describe('WorkItemLinks', () => { expect(findAlert().text()).toBe(errorMessage); }); - it('renders widget child icon and tooltip', () => { - expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m'); - expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close'); - }); - - it('renders confidentiality icon when child item is confidential', () => { - const children = wrapper.findAll('[data-testid="links-child"]'); - const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]'); - - expect(confidentialIcon.exists()).toBe(true); - expect(confidentialIcon.props('name')).toBe('eye-slash'); - }); - it('displays number if children', () => { expect(findChildrenCount().exists()).toBe(true); @@ -195,17 +182,21 @@ describe('WorkItemLinks', () => { }); it('does not display link menu on children', () => { - expect(findFirstLinksMenu().exists()).toBe(false); + expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false); }); }); describe('remove child', () => { + let firstChild; + beforeEach(async () => { await createComponent({ mutationHandler: mutationChangeParentHandler }); + + firstChild = findFirstWorkItemLinkChild(); }); it('calls correct mutation with correct variables', async () => { - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); @@ -220,7 +211,7 @@ describe('WorkItemLinks', () => { }); it('shows toast when mutation succeeds', async () => { - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); @@ -230,28 +221,30 @@ describe('WorkItemLinks', () => { }); it('renders correct number of children after removal', async () => { - expect(findChildren()).toHaveLength(4); + expect(findWorkItemLinkChildItems()).toHaveLength(4); - findFirstLinksMenu().vm.$emit('removeChild'); + firstChild.vm.$emit('remove', firstChild.vm.childItem.id); await waitForPromises(); - expect(findChildren()).toHaveLength(3); + expect(findWorkItemLinkChildItems()).toHaveLength(3); }); }); describe('prefetching child items', () => { + let firstChild; + beforeEach(async () => { await createComponent(); - }); - const findChildLink = () => findChildren().at(0).findComponent(GlButton); + firstChild = findFirstWorkItemLinkChild(); + }); it('does not fetch the child work item before hovering work item links', () => { expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); }); it('fetches the child work item if link is hovered for 250+ ms', async () => { - findChildLink().vm.$emit('mouseover'); + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); await waitForPromises(); @@ -261,9 +254,9 @@ describe('WorkItemLinks', () => { }); it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { - findChildLink().vm.$emit('mouseover'); + firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id); jest.advanceTimersByTime(200); - findChildLink().vm.$emit('mouseout'); + firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id); await waitForPromises(); expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 6a5aa48b610..216c0baa7cc 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -515,6 +515,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = { }, }; +export const workItemTask = { + id: 'gid://gitlab/WorkItem/4', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'bar', + state: 'OPEN', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', +}; + +export const confidentialWorkItemTask = { + id: 'gid://gitlab/WorkItem/2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'xyz', + state: 'OPEN', + confidential: true, + createdAt: '2022-08-03T12:41:54Z', + closedAt: null, + __typename: 'WorkItem', +}; + +export const closedWorkItemTask = { + id: 'gid://gitlab/WorkItem/3', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'abc', + state: 'CLOSED', + confidential: false, + createdAt: '2022-08-03T12:41:54Z', + closedAt: '2022-08-12T13:07:52Z', + __typename: 'WorkItem', +}; + export const workItemHierarchyResponse = { data: { workItem: { @@ -544,45 +586,9 @@ export const workItemHierarchyResponse = { parent: null, children: { nodes: [ - { - id: 'gid://gitlab/WorkItem/2', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'xyz', - state: 'OPEN', - confidential: true, - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - __typename: 'WorkItem', - }, - { - id: 'gid://gitlab/WorkItem/3', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'abc', - state: 'CLOSED', - confidential: false, - createdAt: '2022-08-03T12:41:54Z', - closedAt: '2022-08-12T13:07:52Z', - __typename: 'WorkItem', - }, - { - id: 'gid://gitlab/WorkItem/4', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'bar', - state: 'OPEN', - confidential: false, - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - __typename: 'WorkItem', - }, + confidentialWorkItemTask, + closedWorkItemTask, + workItemTask, { id: 'gid://gitlab/WorkItem/5', workItemType: { diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ac3997fd3ec..361640659b8 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -706,7 +706,7 @@ ProtectedEnvironment: - name - created_at - updated_at -ProtectedEnvironment::DeployAccessLevel: +ProtectedEnvironments::DeployAccessLevel: - id - protected_environment_id - access_level diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb index a3ffd16b895..80368ded830 100644 --- a/spec/requests/api/rpm_project_packages_spec.rb +++ b/spec/requests/api/rpm_project_packages_spec.rb @@ -2,46 +2,187 @@ require 'spec_helper' RSpec.describe API::RpmProjectPackages do - include PackagesManagerApiSpecHelpers - let(:project) { create(:project) } + include HttpBasicAuthHelpers + + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + let(:headers) { {} } let(:package_name) { 'rpm-package.0-1.x86_64.rpm' } let(:package_file_id) { 1 } - shared_examples 'an unimplemented route' do - it_behaves_like 'returning response status', :not_found + shared_examples 'rejects rpm packages access' do |status| + it_behaves_like 'returning response status', status - context 'when feature flag is disabled' do + if status == :unauthorized + it 'has the correct response header' do + subject + + expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"' + end + end + end + + shared_examples 'process rpm packages upload/download' do |status| + it_behaves_like 'returning response status', status + end + + shared_examples 'a deploy token for RPM requests' do + context 'with deploy token headers' do before do - stub_feature_flags(rpm_packages: false) + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) end - it_behaves_like 'returning response status', :not_found + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + context 'when token is valid' do + it_behaves_like 'returning response status', :not_found + end + + context 'when token is invalid' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end end + end + + shared_examples 'a job token for RPM requests' do + context 'with job token headers' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) } - context 'when package feature is disabled' do before do - stub_config(packages: { enabled: false }) + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + project.add_developer(user) end - it_behaves_like 'returning response status', :not_found + context 'with valid token' do + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid token' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'with invalid user' do + let(:headers) { basic_auth_header('foo', job.token) } + + it_behaves_like 'returning response status', :unauthorized + end + end + end + + shared_examples 'a user token for RPM requests' do + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level)) + project.send("add_#{user_role}", user) if member && user_role != :anonymous + end + + it_behaves_like params[:shared_examples_name], params[:expected_status] + end end end describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do - let(:url) { api("/projects/#{project.id}/packages/rpm/repodata/#{package_name}") } + let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" } - subject { get(url, headers: headers) } + subject { get api(url), headers: headers } - it_behaves_like 'an unimplemented route' + it_behaves_like 'a job token for RPM requests' + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a user token for RPM requests' end - describe 'GET /api/v4/projects/:project_id/packages/rpm/:package_file_id/:filename' do - let(:url) { api("/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}") } + describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do + let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" } - subject { get(url, headers: headers) } + subject { get api(url), headers: headers } - it_behaves_like 'an unimplemented route' + it_behaves_like 'a job token for RPM requests' + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a user token for RPM requests' + end + + describe 'POST /api/v4/projects/:project_id/packages/rpm' do + let(:url) { "/projects/#{project.id}/packages/rpm" } + + subject { post api(url), headers: headers } + + context 'with user token' do + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level)) + project.send("add_#{user_role}", user) if member && user_role != :anonymous + end + + it_behaves_like params[:shared_examples_name], params[:expected_status] + end + end + end + + it_behaves_like 'a deploy token for RPM requests' + it_behaves_like 'a job token for RPM requests' end describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do @@ -49,14 +190,22 @@ RSpec.describe API::RpmProjectPackages do subject { post(url, headers: headers) } - it_behaves_like 'an unimplemented route' - end + it_behaves_like 'returning response status', :not_found - describe 'POST /api/v4/projects/:project_id/packages/rpm' do - let(:url) { api("/projects/#{project.id}/packages/rpm") } + context 'when feature flag is disabled' do + before do + stub_feature_flags(rpm_packages: false) + end - subject { post(url, headers: headers) } + it_behaves_like 'returning response status', :not_found + end - it_behaves_like 'an unimplemented route' + context 'when package feature is disabled' do + before do + stub_config(packages: { enabled: false }) + end + + it_behaves_like 'returning response status', :not_found + end end end diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb index df786100b5d..bb087e8778a 100644 --- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb +++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb @@ -98,30 +98,12 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do end context 'when using ETags' do - context 'when etag_merge_request_diff_batches is true' do - it 'does not serialize diffs' do - expect(PaginatedDiffSerializer).not_to receive(:new) + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) - go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) + go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) - expect(response).to have_gitlab_http_status(:not_modified) - end - end - - context 'when etag_merge_request_diff_batches is false' do - before do - stub_feature_flags(etag_merge_request_diff_batches: false) - end - - it 'does not serialize diffs' do - expect_next_instance_of(PaginatedDiffSerializer) do |instance| - expect(instance).not_to receive(:represent) - end - - go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) - - expect(response).to have_gitlab_http_status(:success) - end + expect(response).to have_gitlab_http_status(:not_modified) end end diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb index b373986ca76..c79173e7441 100644 --- a/spec/requests/projects/merge_requests/diffs_spec.rb +++ b/spec/requests/projects/merge_requests/diffs_spec.rb @@ -97,34 +97,14 @@ RSpec.describe 'Merge Requests Diffs' do end context 'when using ETags' do - context 'when etag_merge_request_diff_batches is true' do - let(:headers) { { 'If-None-Match' => response.etag } } - - it 'does not serialize diffs' do - expect(PaginatedDiffSerializer).not_to receive(:new) - - go(headers: headers, page: 0, per_page: 5) - - expect(response).to have_gitlab_http_status(:not_modified) - end - end - - context 'when etag_merge_request_diff_batches is false' do - let(:headers) { { 'If-None-Match' => response.etag } } - - before do - stub_feature_flags(etag_merge_request_diff_batches: false) - end + let(:headers) { { 'If-None-Match' => response.etag } } - it 'does not serialize diffs' do - expect_next_instance_of(PaginatedDiffSerializer) do |instance| - expect(instance).not_to receive(:represent) - end + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) - subject + go(headers: headers, page: 0, per_page: 5) - expect(response).to have_gitlab_http_status(:success) - end + expect(response).to have_gitlab_http_status(:not_modified) end end @@ -279,28 +259,12 @@ RSpec.describe 'Merge Requests Diffs' do context 'when using ETag caching' do let(:headers) { { 'If-None-Match' => response.etag } } - context 'when etag_merge_request_diff_batches is true' do - it 'does not serialize diffs' do - expect(PaginatedDiffSerializer).not_to receive(:new) + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) - subject - - expect(response).to have_gitlab_http_status(:not_modified) - end - end - - context 'when etag_merge_request_diff_batches is false' do - before do - stub_feature_flags(etag_merge_request_diff_batches: false) - end - - it 'does not use cache' do - expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original - - subject + subject - expect(response).to have_gitlab_http_status(:success) - end + expect(response).to have_gitlab_http_status(:not_modified) end end diff --git a/spec/services/ci/queue/pending_builds_strategy_spec.rb b/spec/services/ci/queue/pending_builds_strategy_spec.rb new file mode 100644 index 00000000000..6f22c256c17 --- /dev/null +++ b/spec/services/ci/queue/pending_builds_strategy_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Queue::PendingBuildsStrategy do + let_it_be(:group) { create(:group) } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let!(:build_1) { create(:ci_build, :created, pipeline: pipeline) } + let!(:build_2) { create(:ci_build, :created, pipeline: pipeline) } + let!(:build_3) { create(:ci_build, :created, pipeline: pipeline) } + let!(:pending_build_1) { create(:ci_pending_build, build: build_2, project: project) } + let!(:pending_build_2) { create(:ci_pending_build, build: build_3, project: project) } + let!(:pending_build_3) { create(:ci_pending_build, build: build_1, project: project) } + + describe 'builds_for_group_runner' do + it 'returns builds ordered by build ID' do + strategy = described_class.new(group_runner) + expect(strategy.builds_for_group_runner).to eq([pending_build_3, pending_build_1, pending_build_2]) + end + end +end diff --git a/workhorse/go.mod b/workhorse/go.mod index c264dacbee6..7b57cf0e8fd 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -34,7 +34,7 @@ require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/net v0.0.0-20220531201128-c960675eff93 golang.org/x/tools v0.1.11 - google.golang.org/grpc v1.48.0 + google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 honnef.co/go/tools v0.3.3 ) diff --git a/workhorse/go.sum b/workhorse/go.sum index 7ba967c4d33..8bd801b0522 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1780,8 +1780,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |