Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-01 18:12:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-01 18:12:07 +0300
commit7b197a72aac71c97cf234401a585aba58841ed64 (patch)
tree29212c9616269c257430a8cb5c247e45cbf17c7c
parenta53033814ddff597cd05244f378915bacdcb5aea (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml8
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue11
-rw-r--r--app/assets/javascripts/issues/show/components/delete_issue_modal.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/edit_actions.vue86
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue53
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue109
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue70
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql2
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb4
-rw-r--r--app/services/ci/queue/pending_builds_strategy.rb6
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--config/feature_categories.yml1
-rw-r--r--config/feature_flags/development/ci_cost_factors_narrow_os_contribution_by_plan.yml (renamed from config/feature_flags/development/etag_merge_request_diff_batches.yml)10
-rw-r--r--config/feature_flags/development/order_builds_for_group_runner.yml8
-rw-r--r--db/docs/protected_environment_deploy_access_levels.yml2
-rw-r--r--doc/.vale/gitlab/InternalLinkExtension.yml4
-rw-r--r--doc/api/group_protected_environments.md2
-rw-r--r--doc/api/protected_environments.md2
-rw-r--r--doc/ci/cloud_services/index.md2
-rw-r--r--doc/ci/environments/deployment_safety.md4
-rw-r--r--doc/ci/testing/unit_test_reports.md2
-rw-r--r--doc/ci/yaml/artifacts_reports.md2
-rw-r--r--doc/integration/jenkins.md2
-rw-r--r--doc/security/img/allowlist_v13_0.pngbin16076 -> 0 bytes
-rw-r--r--doc/security/webhooks.md109
-rw-r--r--doc/user/project/import/github.md2
-rw-r--r--lib/api/rpm_project_packages.rb18
-rw-r--r--locale/gitlab.pot12
-rw-r--r--package.json2
-rw-r--r--spec/frontend/issues/show/components/edit_actions_spec.js82
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap386
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js122
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js51
-rw-r--r--spec/frontend/work_items/mock_data.js84
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/requests/api/rpm_project_packages_spec.rb195
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb26
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb54
-rw-r--r--spec/services/ci/queue/pending_builds_strategy_spec.rb24
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum3
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
deleted file mode 100644
index 973b53a57a4..00000000000
--- a/doc/security/img/allowlist_v13_0.png
+++ /dev/null
Binary files differ
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=