summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-03 18:10:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-03 18:10:36 +0000
commitfa885192b7d34ba910d1be629ad58c18148a86f1 (patch)
treeea4316b12a15d6767d8cc12cee4f9feb5a076d91
parentaf7ba639ec0b6bba26adc244e8971d4113d2c041 (diff)
downloadgitlab-ce-fa885192b7d34ba910d1be629ad58c18148a86f1.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue (renamed from app/assets/javascripts/content_editor/components/wrappers/table_cell.vue)23
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue23
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue23
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js5
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js34
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js38
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue148
-rw-r--r--app/models/concerns/approvable_base.rb4
-rw-r--r--app/services/pages/legacy_storage_lease.rb19
-rw-r--r--app/services/pages/migrate_legacy_storage_to_deployment_service.rb15
-rw-r--r--app/views/admin/application_settings/_eks.html.haml8
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--config/metrics/counts_28d/20210902191057_i_quickactions_unapprove_monthly.yml28
-rw-r--r--config/metrics/counts_7d/20210902191054_i_quickactions_unapprove_weekly.yml28
-rw-r--r--doc/api/index.md122
-rw-r--r--doc/api/releases/index.md2
-rw-r--r--doc/ci/jobs/ci_job_token.md125
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/development/packages.md2
-rw-r--r--doc/security/token_overview.md6
-rw-r--r--doc/update/index.md4
-rw-r--r--doc/update/plan_your_upgrade.md181
-rw-r--r--doc/user/packages/container_registry/index.md12
-rw-r--r--doc/user/packages/debian_repository/index.md2
-rw-r--r--doc/user/packages/generic_packages/index.md4
-rw-r--r--doc/user/packages/helm_repository/index.md2
-rw-r--r--doc/user/packages/terraform_module_registry/index.md2
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb14
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml4
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js (renamed from spec/frontend/content_editor/components/wrappers/table_cell_spec.js)47
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js37
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js37
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js137
-rw-r--r--spec/models/concerns/approvable_base_spec.rb28
-rw-r--r--spec/services/pages/legacy_storage_lease_spec.rb65
-rw-r--r--spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb8
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb72
42 files changed, 1087 insertions, 279 deletions
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 5cc88ef357a..c44e8145982 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -4,8 +4,11 @@ import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { __ } from '~/locale';
+const TABLE_CELL_HEADER = 'th';
+const TABLE_CELL_BODY = 'td';
+
export default {
- name: 'TableCellWrapper',
+ name: 'TableCellBaseWrapper',
components: {
NodeViewWrapper,
NodeViewContent,
@@ -14,6 +17,11 @@ export default {
GlDropdownDivider,
},
props: {
+ cellType: {
+ type: String,
+ validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
+ required: true,
+ },
editor: {
type: Object,
required: true,
@@ -37,6 +45,9 @@ export default {
totalCols() {
return this.selectedRect?.map.width;
},
+ isTableBodyCell() {
+ return this.cellType === TABLE_CELL_BODY;
+ },
},
mounted() {
this.editor.on('selectionUpdate', this.handleSelectionUpdate);
@@ -83,7 +94,11 @@ export default {
};
</script>
<template>
- <node-view-wrapper class="gl-relative gl-padding-5 gl-min-w-10" as="td" @click="hideDropdown">
+ <node-view-wrapper
+ class="gl-relative gl-padding-5 gl-min-w-10"
+ :as="cellType"
+ @click="hideDropdown"
+ >
<span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
<gl-dropdown
ref="dropdown"
@@ -104,14 +119,14 @@ export default {
<gl-dropdown-item @click="runCommand('addColumnAfter')">
{{ $options.i18n.insertColumnAfter }}
</gl-dropdown-item>
- <gl-dropdown-item @click="runCommand('addRowBefore')">
+ <gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
{{ $options.i18n.insertRowBefore }}
</gl-dropdown-item>
<gl-dropdown-item @click="runCommand('addRowAfter')">
{{ $options.i18n.insertRowAfter }}
</gl-dropdown-item>
<gl-dropdown-divider />
- <gl-dropdown-item v-if="totalRows > 2" @click="runCommand('deleteRow')">
+ <gl-dropdown-item v-if="totalRows > 2 && isTableBodyCell" @click="runCommand('deleteRow')">
{{ $options.i18n.deleteRow }}
</gl-dropdown-item>
<gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')">
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
new file mode 100644
index 00000000000..6b4343dd5b8
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
@@ -0,0 +1,23 @@
+<script>
+import TableCellBase from './table_cell_base.vue';
+
+export default {
+ name: 'TableCellBody',
+ components: {
+ TableCellBase,
+ },
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <table-cell-base cell-type="td" v-bind="$props" />
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
new file mode 100644
index 00000000000..5f9889374f6
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
@@ -0,0 +1,23 @@
+<script>
+import TableCellBase from './table_cell_base.vue';
+
+export default {
+ name: 'TableCellHeader',
+ components: {
+ TableCellBase,
+ },
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <table-cell-base cell-type="th" v-bind="$props" />
+</template>
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index 3008ac0afdb..befc33e669f 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,12 +1,12 @@
import { TableCell } from '@tiptap/extension-table-cell';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import TableCellWrapper from '../components/wrappers/table_cell.vue';
+import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
addNodeView() {
- return VueNodeViewRenderer(TableCellWrapper);
+ return VueNodeViewRenderer(TableCellBodyWrapper);
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 513e3da4706..829b06fc14b 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,6 +1,11 @@
import { TableHeader } from '@tiptap/extension-table-header';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ addNodeView() {
+ return VueNodeViewRenderer(TableCellHeaderWrapper);
+ },
});
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index 7e7f0572faf..48658c7ffe0 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -52,6 +52,19 @@ export const createNodeDict = (nodes) => {
}, {});
};
+/*
+ A peformant alternative to lodash's isEqual. Because findIndex always finds
+ the first instance of a match, if the found index is not the first, we know
+ it is in fact a duplicate.
+*/
+const deduplicate = (item, itemIndex, arr) => {
+ const foundIdx = arr.findIndex((test) => {
+ return test.source === item.source && test.target === item.target;
+ });
+
+ return foundIdx === itemIndex;
+};
+
export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
@@ -83,7 +96,8 @@ export const getAllAncestors = (nodes, nodeDict) => {
return nodeDict[node]?.needs || '';
})
.flat()
- .filter(Boolean);
+ .filter(Boolean)
+ .filter(deduplicate);
if (needs.length) {
return [...needs, ...getAllAncestors(needs, nodeDict)];
@@ -108,29 +122,15 @@ export const filterByAncestors = (links, nodeDict) =>
const targetNode = target;
const targetNodeNeeds = nodeDict[targetNode].needs;
const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
-
const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
return !allAncestors.includes(source);
});
-/*
- A peformant alternative to lodash's isEqual. Because findIndex always finds
- the first instance of a match, if the found index is not the first, we know
- it is in fact a duplicate.
-*/
-const deduplicate = (item, itemIndex, arr) => {
- const foundIdx = arr.findIndex((test) => {
- return test.source === item.source && test.target === item.target;
- });
-
- return foundIdx === itemIndex;
-};
-
export const parseData = (nodes) => {
const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
- const filteredLinks = filterByAncestors(allLinks, nodeDict);
- const links = filteredLinks.filter(deduplicate);
+ const filteredLinks = allLinks.filter(deduplicate);
+ const links = filterByAncestors(filteredLinks, nodeDict);
return { nodes, links };
};
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 665b0698cc0..1d79818cbe8 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -118,7 +118,7 @@ export default {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
},
isBinaryFileType() {
- return this.isBinary || this.viewer.fileType === 'download';
+ return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
},
blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes || [];
@@ -180,7 +180,7 @@ export default {
<div v-if="blobInfo && !isLoading" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer || isBinary"
+ :hide-viewer-switcher="!hasRichViewer || isBinaryFileType"
:is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@@ -188,7 +188,7 @@ export default {
>
<template #actions>
<blob-edit
- :show-edit-button="!isBinary"
+ :show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
/>
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
new file mode 100644
index 00000000000..00aa5519ec6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
@@ -0,0 +1,38 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import '@gitlab/ui/dist/utility_classes.css';
+import UsageGraph from './usage_graph.vue';
+
+export default {
+ component: UsageGraph,
+ title: 'vue_shared/components/storage_counter/usage_graph',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { UsageGraph },
+ props: Object.keys(argTypes),
+ template: '<usage-graph v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.argTypes = {
+ rootStorageStatistics: {
+ description: 'The statistics object with all its fields',
+ type: { name: 'object', required: true },
+ defaultValue: {
+ buildArtifactsSize: 400000,
+ pipelineArtifactsSize: 38000,
+ lfsObjectsSize: 4800000,
+ packagesSize: 3800000,
+ repositorySize: 39000000,
+ snippetsSize: 2000112,
+ storageSize: 39930000,
+ uploadsSize: 7000,
+ wikiSize: 300000,
+ },
+ },
+ limit: {
+ description:
+ 'When a limit is set, users will see how much of their storage usage (limit) is used. In case the limit is 0 or the current usage exceeds the limit, it just renders the distribution',
+ defaultValue: 0,
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
new file mode 100644
index 00000000000..c33d065ff4b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
@@ -0,0 +1,148 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ rootStorageStatistics: {
+ required: true,
+ type: Object,
+ },
+ limit: {
+ required: true,
+ type: Number,
+ },
+ },
+ computed: {
+ storageTypes() {
+ const {
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ storageSize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = this.rootStorageStatistics;
+ const artifactsSize = buildArtifactsSize + pipelineArtifactsSize;
+
+ if (storageSize === 0) {
+ return null;
+ }
+
+ return [
+ {
+ name: s__('UsageQuota|Repositories'),
+ style: this.usageStyle(this.barRatio(repositorySize)),
+ class: 'gl-bg-data-viz-blue-500',
+ size: repositorySize,
+ },
+ {
+ name: s__('UsageQuota|LFS Objects'),
+ style: this.usageStyle(this.barRatio(lfsObjectsSize)),
+ class: 'gl-bg-data-viz-orange-600',
+ size: lfsObjectsSize,
+ },
+ {
+ name: s__('UsageQuota|Packages'),
+ style: this.usageStyle(this.barRatio(packagesSize)),
+ class: 'gl-bg-data-viz-aqua-500',
+ size: packagesSize,
+ },
+ {
+ name: s__('UsageQuota|Artifacts'),
+ style: this.usageStyle(this.barRatio(artifactsSize)),
+ class: 'gl-bg-data-viz-green-600',
+ size: artifactsSize,
+ tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'),
+ },
+ {
+ name: s__('UsageQuota|Wikis'),
+ style: this.usageStyle(this.barRatio(wikiSize)),
+ class: 'gl-bg-data-viz-magenta-500',
+ size: wikiSize,
+ },
+ {
+ name: s__('UsageQuota|Snippets'),
+ style: this.usageStyle(this.barRatio(snippetsSize)),
+ class: 'gl-bg-data-viz-orange-800',
+ size: snippetsSize,
+ },
+ {
+ name: s__('UsageQuota|Uploads'),
+ style: this.usageStyle(this.barRatio(uploadsSize)),
+ class: 'gl-bg-data-viz-aqua-700',
+ size: uploadsSize,
+ },
+ ]
+ .filter((data) => data.size !== 0)
+ .sort((a, b) => b.size - a.size);
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ usageStyle(ratio) {
+ return { flex: ratio };
+ },
+ barRatio(size) {
+ let max = this.rootStorageStatistics.storageSize;
+
+ if (this.limit !== 0 && max <= this.limit) {
+ max = this.limit;
+ }
+
+ return size / max;
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
+ <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="storage-type-usage gl-h-full gl-display-inline-block"
+ :class="storageType.class"
+ :style="storageType.style"
+ data-testid="storage-type-usage"
+ ></div>
+ </div>
+ <div class="row py-0">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="col-md-auto gl-display-flex gl-align-items-center"
+ data-testid="storage-type-legend"
+ >
+ <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
+ <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
+ {{ storageType.name }}
+ </span>
+ <span class="gl-text-gray-500 gl-font-sm">
+ {{ formatSize(storageType.size) }}
+ </span>
+ <span
+ v-if="storageType.tooltip"
+ v-gl-tooltip
+ :title="storageType.tooltip"
+ :aria-label="storageType.tooltip"
+ class="gl-ml-2"
+ >
+ <gl-icon name="question" :size="12" />
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index ef7ba7b1089..8240f9bd6ea 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -54,4 +54,8 @@ module ApprovableBase
def can_be_approved_by?(user)
user && !approved_by?(user) && user.can?(:approve_merge_request, self)
end
+
+ def can_be_unapproved_by?(user)
+ user && approved_by?(user) && user.can?(:approve_merge_request, self)
+ end
end
diff --git a/app/services/pages/legacy_storage_lease.rb b/app/services/pages/legacy_storage_lease.rb
deleted file mode 100644
index 1849def0183..00000000000
--- a/app/services/pages/legacy_storage_lease.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Pages
- module LegacyStorageLease
- extend ActiveSupport::Concern
-
- include ::ExclusiveLeaseGuard
-
- LEASE_TIMEOUT = 1.hour
-
- def lease_key
- "pages_legacy_storage:#{project.id}"
- end
-
- def lease_timeout
- LEASE_TIMEOUT
- end
- end
-end
diff --git a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
index 95c7107eb62..9c1671fbc15 100644
--- a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
+++ b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
@@ -2,10 +2,7 @@
module Pages
class MigrateLegacyStorageToDeploymentService
- ExclusiveLeaseTakenError = Class.new(StandardError)
-
include BaseServiceUtility
- include ::Pages::LegacyStorageLease
attr_reader :project
@@ -16,18 +13,6 @@ module Pages
end
def execute
- result = try_obtain_lease do
- execute_unsafe
- end
-
- raise ExclusiveLeaseTakenError, "Can't migrate pages for project #{project.id}: exclusive lease taken" if result.nil?
-
- result
- end
-
- private
-
- def execute_unsafe
zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
if zip_result[:status] == :error
diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml
index 64cc23844e5..c83e28d7f0b 100644
--- a/app/views/admin/application_settings/_eks.html.haml
+++ b/app/views/admin/application_settings/_eks.html.haml
@@ -17,18 +17,18 @@
.form-check
= f.check_box :eks_integration_enabled, class: 'form-check-input'
= f.label :eks_integration_enabled, class: 'form-check-label' do
- Enable Amazon EKS integration
+ = _('Enable Amazon EKS integration')
.form-group
- = f.label :eks_account_id, 'Account ID', class: 'label-bold'
+ = f.label :eks_account_id, _('Account ID'), class: 'label-bold'
= f.text_field :eks_account_id, class: 'form-control gl-form-input'
.form-group
- = f.label :eks_access_key_id, 'Access key ID', class: 'label-bold'
+ = f.label :eks_access_key_id, _('Access key ID'), class: 'label-bold'
= f.text_field :eks_access_key_id, class: 'form-control gl-form-input'
.form-text.text-muted
= _('AWS Access Key. Only required if not using role instance credentials')
.form-group
- = f.label :eks_secret_access_key, 'Secret access key', class: 'label-bold'
+ = f.label :eks_secret_access_key, _('Secret access key'), class: 'label-bold'
= f.password_field :eks_secret_access_key, autocomplete: 'off', class: 'form-control gl-form-input'
.form-text.text-muted
= _('AWS Secret Access Key. Only required if not using role instance credentials')
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 70626636ac0..75bd985560b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -105,6 +105,6 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Control which projects can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API.")
- = link_to _('Learn more'), help_page_path('api/index', anchor: 'limit-gitlab-cicd-job-token-access'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more'), help_page_path('ci/jobs/ci_job_token'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'ci/token_access/index'
diff --git a/config/metrics/counts_28d/20210902191057_i_quickactions_unapprove_monthly.yml b/config/metrics/counts_28d/20210902191057_i_quickactions_unapprove_monthly.yml
new file mode 100644
index 00000000000..63561617139
--- /dev/null
+++ b/config/metrics/counts_28d/20210902191057_i_quickactions_unapprove_monthly.yml
@@ -0,0 +1,28 @@
+---
+data_category: optional
+key_path: redis_hll_counters.quickactions.i_quickactions_unapprove_monthly
+description: Count of MAU using the `/unapprove` quick action
+product_section: dev
+product_stage: create
+product_group: group::code review
+product_category: code_review
+value_type: number
+status: data_available
+milestone: "14.3"
+time_frame: 28d
+data_source: redis_hll
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_unapprove
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_unapprove
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/config/metrics/counts_7d/20210902191054_i_quickactions_unapprove_weekly.yml b/config/metrics/counts_7d/20210902191054_i_quickactions_unapprove_weekly.yml
new file mode 100644
index 00000000000..70de82e6925
--- /dev/null
+++ b/config/metrics/counts_7d/20210902191054_i_quickactions_unapprove_weekly.yml
@@ -0,0 +1,28 @@
+---
+data_category: optional
+key_path: redis_hll_counters.quickactions.i_quickactions_unapprove_weekly
+description: Count of WAU using the `/unapprove` quick action
+product_section: dev
+product_stage: create
+product_group: group::code review
+product_category: code_review
+value_type: number
+status: data_available
+milestone: "14.3"
+time_frame: 7d
+data_source: redis_hll
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_unapprove
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_unapprove
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/doc/api/index.md b/doc/api/index.md
index 92af68645ef..f7148dcf472 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -125,7 +125,7 @@ There are several ways you can authenticate with the GitLab API:
- [Personal access tokens](../user/profile/personal_access_tokens.md)
- [Project access tokens](../user/project/settings/project_access_tokens.md)
- [Session cookie](#session-cookie)
-- [GitLab CI/CD job token](#gitlab-cicd-job-token) **(Specific endpoints only)**
+- [GitLab CI/CD job token](../ci/jobs/ci_job_token.md) **(Specific endpoints only)**
Project access tokens are supported by:
@@ -208,126 +208,6 @@ The primary user of this authentication method is the web frontend of GitLab
itself. The web frontend can use the API as the authenticated user to get a
list of projects without explicitly passing an access token.
-### GitLab CI/CD job token
-
-When a pipeline job is about to run, GitLab generates a unique token and injects it as the
-[`CI_JOB_TOKEN` predefined variable](../ci/variables/predefined_variables.md).
-
-You can use a GitLab CI/CD job token to authenticate with specific API endpoints:
-
-- Packages:
- - [Package Registry](../user/packages/package_registry/index.md). To push to the
- Package Registry, you can use [deploy tokens](../user/project/deploy_tokens/index.md).
- - [Container Registry](../user/packages/container_registry/index.md)
- (the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
- - [Container Registry API](container_registry.md)
- (scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
-- [Get job artifacts](job_artifacts.md#get-job-artifacts).
-- [Get job token's job](jobs.md#get-job-tokens-job).
-- [Pipeline triggers](pipeline_triggers.md), using the `token=` parameter.
-- [Release creation](releases/index.md#create-a-release).
-- [Terraform plan](../user/infrastructure/index.md).
-
-The token has the same permissions to access the API as the user that triggers the
-pipeline. Therefore, this user must be assigned to [a role that has the required privileges](../user/permissions.md).
-
-The token is valid only while the pipeline job runs. After the job finishes, you can't
-use the token anymore.
-
-A job token can access a project's resources without any configuration, but it might
-give extra permissions that aren't necessary. There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559)
-to redesign the feature for more strategic control of the access permissions.
-
-You can also use the job token to authenticate and clone a repository from a private project
-in a CI/CD job:
-
-```shell
-git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/<namespace>/<project>
-```
-
-#### GitLab CI/CD job token security
-
-To make sure that this token doesn't leak, GitLab:
-
-- Masks the job token in job logs.
-- Grants permissions to the job token only when the job is running.
-
-To make sure that this token doesn't leak, you should also configure
-your [runners](../ci/runners/README.md) to be secure. Avoid:
-
-- Using Docker's `privileged` mode if the machines are re-used.
-- Using the [`shell` executor](https://docs.gitlab.com/runner/executors/shell.html) when jobs
- run on the same machine.
-
-If you have an insecure GitLab Runner configuration, you increase the risk that someone
-tries to steal tokens from other jobs.
-
-#### Limit GitLab CI/CD job token access
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1.
-> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
-> - Disabled on GitLab.com.
-> - Not recommended for production use.
-> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-ci-job-token-scope-limit). **(FREE SELF)**
-
-This in-development feature might not be available for your use. There can be
-[risks when enabling features still in development](../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
-Refer to this feature's version history for more details.
-
-You can limit the access scope of a project's CI/CD job token to increase the
-job token's security. A job token might give extra permissions that aren't necessary
-to access specific private resources. Limiting the job token access scope reduces the risk of a leaked
-token being used to access private data that the user associated to the job can access.
-
-Control the job token access scope with an allowlist of other projects authorized
-to be accessed by authenticating with the current project's job token. By default
-the token scope only allows access to the same project where the token comes from.
-Other projects can be added and removed by maintainers with access to both projects.
-
-This setting is enabled by default for all new projects, and disabled by default in projects
-created before GitLab 14.1. It is strongly recommended that project maintainers enable this
-setting at all times, and configure the allowlist for cross-project access if needed.
-
-For example, when the setting is enabled, jobs in a pipeline in project `A` have
-a `CI_JOB_TOKEN` scope limited to project `A`. If the job needs to use the token
-to make an API request to a private project `B`, then `B` must be added to the allowlist for `A`.
-If project `B` is public or internal, it doesn't need to be added to the allowlist.
-The job token scope is only for controlling access to private projects.
-
-To enable and configure the job token scope limit:
-
-1. On the top bar, select **Menu > Projects** and find your project.
-1. On the left sidebar, select **Settings > CI/CD**.
-1. Expand **Token Access**.
-1. Toggle **Limit CI_JOB_TOKEN access** to enabled.
-1. (Optional) Add existing projects to the token's access scope. The user adding a
- project must have the [maintainer role](../user/permissions.md) in both projects.
-
-If the job token scope limit is disabled, the token can potentially be used to authenticate
-API requests to all projects accessible to the user that triggered the job.
-
-There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to improve
-the feature with more strategic control of the access permissions.
-
-##### Enable or disable CI job token scope limit **(FREE SELF)**
-
-The GitLab CI/CD job token access scope limit is under development and not ready for production
-use. It is deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
-can enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:ci_scoped_job_token)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:ci_scoped_job_token)
-```
-
### Impersonation tokens
Impersonation tokens are a type of [personal access token](../user/profile/personal_access_tokens.md).
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 35bf24c586c..da9f1435187 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -21,7 +21,7 @@ For authentication, the Releases API accepts either:
- A [Personal Access Token](../../user/profile/personal_access_tokens.md) using the
`PRIVATE-TOKEN` header.
-- The [GitLab CI/CD job token](../index.md#gitlab-cicd-job-token) `$CI_JOB_TOKEN` using
+- The [GitLab CI/CD job token](../../ci/jobs/ci_job_token.md) `$CI_JOB_TOKEN` using
the `JOB-TOKEN` header.
## List Releases
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
new file mode 100644
index 00000000000..b98dfa1ff69
--- /dev/null
+++ b/doc/ci/jobs/ci_job_token.md
@@ -0,0 +1,125 @@
+---
+stage: Verify
+group: Pipeline Execution
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# GitLab CI/CD job token
+
+When a pipeline job is about to run, GitLab generates a unique token and injects it as the
+[`CI_JOB_TOKEN` predefined variable](../variables/predefined_variables.md).
+
+You can use a GitLab CI/CD job token to authenticate with specific API endpoints:
+
+- Packages:
+ - [Package Registry](../../user/packages/package_registry/index.md). To push to the
+ Package Registry, you can use [deploy tokens](../../user/project/deploy_tokens/index.md).
+ - [Container Registry](../../user/packages/container_registry/index.md)
+ (the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
+ - [Container Registry API](../../api/container_registry.md)
+ (scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
+- [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts).
+- [Get job token's job](../../api/jobs.md#get-job-tokens-job).
+- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter.
+- [Release creation](../../api/releases/index.md#create-a-release).
+- [Terraform plan](../../user/infrastructure/index.md).
+
+The token has the same permissions to access the API as the user that triggers the
+pipeline. Therefore, this user must be assigned to [a role that has the required privileges](../../user/permissions.md).
+
+The token is valid only while the pipeline job runs. After the job finishes, you can't
+use the token anymore.
+
+A job token can access a project's resources without any configuration, but it might
+give extra permissions that aren't necessary. There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559)
+to redesign the feature for more strategic control of the access permissions.
+
+You can also use the job token to authenticate and clone a repository from a private project
+in a CI/CD job:
+
+```shell
+git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/<namespace>/<project>
+```
+
+## GitLab CI/CD job token security
+
+To make sure that this token doesn't leak, GitLab:
+
+- Masks the job token in job logs.
+- Grants permissions to the job token only when the job is running.
+
+To make sure that this token doesn't leak, you should also configure
+your [runners](../runners/index.md) to be secure. Avoid:
+
+- Using Docker's `privileged` mode if the machines are re-used.
+- Using the [`shell` executor](https://docs.gitlab.com/runner/executors/shell.html) when jobs
+ run on the same machine.
+
+If you have an insecure GitLab Runner configuration, you increase the risk that someone
+tries to steal tokens from other jobs.
+
+## Limit GitLab CI/CD job token access
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1.
+> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
+> - Disabled on GitLab.com.
+> - Not recommended for production use.
+> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-ci-job-token-scope-limit). **(FREE SELF)**
+
+This in-development feature might not be available for your use. There can be
+[risks when enabling features still in development](../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
+Refer to this feature's version history for more details.
+
+You can limit the access scope of a project's CI/CD job token to increase the
+job token's security. A job token might give extra permissions that aren't necessary
+to access specific private resources. Limiting the job token access scope reduces the risk of a leaked
+token being used to access private data that the user associated to the job can access.
+
+Control the job token access scope with an allowlist of other projects authorized
+to be accessed by authenticating with the current project's job token. By default
+the token scope only allows access to the same project where the token comes from.
+Other projects can be added and removed by maintainers with access to both projects.
+
+This setting is enabled by default for all new projects, and disabled by default in projects
+created before GitLab 14.1. It is strongly recommended that project maintainers enable this
+setting at all times, and configure the allowlist for cross-project access if needed.
+
+For example, when the setting is enabled, jobs in a pipeline in project `A` have
+a `CI_JOB_TOKEN` scope limited to project `A`. If the job needs to use the token
+to make an API request to a private project `B`, then `B` must be added to the allowlist for `A`.
+If project `B` is public or internal, it doesn't need to be added to the allowlist.
+The job token scope is only for controlling access to private projects.
+
+To enable and configure the job token scope limit:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > CI/CD**.
+1. Expand **Token Access**.
+1. Toggle **Limit CI_JOB_TOKEN access** to enabled.
+1. (Optional) Add existing projects to the token's access scope. The user adding a
+ project must have the [maintainer role](../../user/permissions.md) in both projects.
+
+If the job token scope limit is disabled, the token can potentially be used to authenticate
+API requests to all projects accessible to the user that triggered the job.
+
+There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to improve
+the feature with more strategic control of the access permissions.
+
+### Enable or disable CI job token scope limit **(FREE SELF)**
+
+The GitLab CI/CD job token access scope limit is under development and not ready for production
+use. It is deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_scoped_job_token)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_scoped_job_token)
+```
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index ab9a548eb6e..50c3bce044d 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -63,7 +63,7 @@ There are also [Kubernetes-specific deployment variables](../../user/project/clu
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job. |
| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the job's stage. |
| `CI_JOB_STATUS` | all | 13.5 | The status of the job as each runner stage is executed. Use with [`after_script`](../yaml/index.md#after_script). Can be `success`, `failed`, or `canceled`. |
-| `CI_JOB_TOKEN` | 9.0 | 1.2 | A token to authenticate with [certain API endpoints](../../api/index.md#gitlab-cicd-job-token). The token is valid as long as the job is running. |
+| `CI_JOB_TOKEN` | 9.0 | 1.2 | A token to authenticate with [certain API endpoints](../jobs/ci_job_token.md). The token is valid as long as the job is running. |
| `CI_JOB_URL` | 11.1 | 0.5 | The job details URL. |
| `CI_JOB_STARTED_AT` | 13.10 | all | The UTC datetime when a job started, in [ISO 8601](https://tools.ietf.org/html/rfc3339#appendix-A) format. |
| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Only available if the pipeline has a Kubernetes cluster available for deployments. `true` when available. |
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 94882cefc30..869a1755d8f 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -133,7 +133,7 @@ During this phase, the idea is to collect as much information as possible about
- **Authentication**: What authentication mechanisms are available (OAuth, Basic
Authorization, other). Keep in mind that GitLab users often want to use their
[Personal Access Tokens](../user/profile/personal_access_tokens.md).
- Although not needed for the MVC first iteration, the [CI/CD job tokens](../api/index.md#gitlab-cicd-job-token)
+ Although not needed for the MVC first iteration, the [CI/CD job tokens](../ci/jobs/ci_job_token.md)
have to be supported at some point in the future.
- **Requests**: Which requests are needed to have a working MVC. Ideally, produce
a list of all the requests needed for the MVC (including required actions). Further
diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md
index c00e5bff383..4e72033fd77 100644
--- a/doc/security/token_overview.md
+++ b/doc/security/token_overview.md
@@ -71,7 +71,7 @@ You can use the runner registration token to add runners that execute jobs in a
After registration, the runner receives an authentication token, which it uses to authenticate with GitLab when picking up jobs from the job queue. The authentication token is stored locally in the runner's [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html) file.
-After authentication with GitLab, the runner receives a [job token](../api/index.md#gitlab-cicd-job-token), which it uses to execute the job.
+After authentication with GitLab, the runner receives a [job token](../ci/jobs/ci_job_token.md), which it uses to execute the job.
In case of Docker Machine/Kubernetes/VirtualBox/Parallels/SSH executors, the execution environment has no access to the runner authentication token, because it stays on the runner machine. They have access to the job token only, which is needed to execute the job.
@@ -79,7 +79,7 @@ Malicious access to a runner's file system may expose the `config.toml` file and
## CI/CD job tokens
-The [CI/CD](../api/index.md#gitlab-cicd-job-token) job token
+The [CI/CD](../ci/jobs/ci_job_token.md) job token
is a short lived token only valid for the duration of a job. It gives a CI/CD job
access to a limited amount of API endpoints.
API authentication uses the job token, by using the authorization of the user
@@ -105,7 +105,7 @@ This table shows available scopes per token. Scopes can be limited further on to
1. Limited to the one project.
1. Runner registration and authentication token don't provide direct access to repositories, but can be used to register and authenticate a new runner that may execute jobs which do have access to the repository
-1. Limited to certain [endpoints](../api/index.md#gitlab-cicd-job-token).
+1. Limited to certain [endpoints](../ci/jobs/ci_job_token.md).
## Security considerations
diff --git a/doc/update/index.md b/doc/update/index.md
index e714c3a9059..8851d51d4d1 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -70,6 +70,10 @@ Instructions on how to update a cloud-native deployment are in
Use the [version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html)
from the chart version to GitLab version to determine the [upgrade path](#upgrade-paths).
+## Plan your upgrade
+
+See the guide to [plan your GitLab upgrade](plan_your_upgrade.md).
+
## Checking for background migrations before upgrading
Certain major/minor releases may require different migrations to be
diff --git a/doc/update/plan_your_upgrade.md b/doc/update/plan_your_upgrade.md
new file mode 100644
index 00000000000..d90516589e7
--- /dev/null
+++ b/doc/update/plan_your_upgrade.md
@@ -0,0 +1,181 @@
+---
+stage: Enablement
+group: Distribution
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Create a GitLab upgrade plan
+
+This document serves as a guide to create a strong plan to upgrade a self-managed
+GitLab instance.
+
+General notes:
+
+- If possible, we recommend you test out the upgrade in a test environment before
+ updating your production instance. Ideally, your test environment should mimic
+ your production environment as closely as possible.
+- If [working with Support](https://about.gitlab.com/support/scheduling-live-upgrade-assistance.html)
+ to create your plan, share details of your architecture, including:
+ - How is GitLab installed?
+ - What is the operating system of the node?
+ (check [OS versions that are no longer supported](https://docs.gitlab.com/omnibus/package-information/deprecated_os.html) to confirm that later updates are available).
+ - Is it a single-node or a multi-node setup? If multi-node, share any architectural details about each node with us.
+ - Are you using [GitLab Geo](../administration/geo/index.md)? If so, share any architectural details about each secondary node.
+ - What else might be unique or interesting in your setup that might be important for us to understand?
+ - Are you running into any known issues with your current version of GitLab?
+
+## Pre-upgrade and post-upgrade checks
+
+Immediately before and after the upgrade, perform the pre-upgrade and post-upgrade checks
+to ensure the major components of GitLab are working:
+
+1. [Check the general configuration](../administration/raketasks/maintenance.md#check-gitlab-configuration):
+
+ ```shell
+ sudo gitlab-rake gitlab:check
+ ```
+
+1. Confirm that encrypted database values [can be decrypted](../administration/raketasks/doctor.md#verify-database-values-can-be-decrypted-using-the-current-secrets):
+
+ ```shell
+ sudo gitlab-rake gitlab:doctor:secrets
+ ```
+
+1. In GitLab UI, check that:
+ - Users can log in.
+ - The project list is visible.
+ - Project issues and merge requests are accessible.
+ - Users can clone repositories from GitLab.
+ - Users can push commits to GitLab.
+
+1. For GitLab CI/CD, check that:
+ - Runners pick up jobs.
+ - Docker images can be pushed and pulled from the registry.
+
+1. If using Geo, run the relevant checks on the primary and each secondary:
+
+ ```shell
+ sudo gitlab-rake gitlab:geo:check
+ ```
+
+1. If using Elasticsearch, verify that searches are successful.
+
+If in any case something goes wrong, see [how to troubleshoot](#troubleshooting).
+
+## Rollback plan
+
+It's possible that something may go wrong during an upgrade, so it's critical
+that a rollback plan be present for that scenario. A proper rollback plan
+creates a clear path to bring the instance back to its last working state. It is
+comprised of a way to back up the instance and a way to restore it.
+
+### Back up GitLab
+
+Create a backup of GitLab and all its data (database, repos, uploads, builds,
+artifacts, LFS objects, registry, pages). This is vital for making it possible
+to roll back GitLab to a working state if there's a problem with the upgrade:
+
+- Create a [GitLab backup](../raketasks/backup_restore.md#back-up-gitlab).
+ Make sure to follow the instructions based on your installation method.
+ Don't forget to back up the [secrets and configuration files](../raketasks/backup_restore.md#storing-configuration-files).
+- Alternatively, create a snapshot of your instance. If this is a multi-node
+ installation, you must snapshot every node.
+ **This process is out of scope for GitLab Support.**
+
+### Restore GitLab
+
+To restore your GitLab backup:
+
+- Before restoring, make sure to read about the
+ [prerequisites](../raketasks/backup_restore.md#restore-gitlab), most importantly,
+ the versions of the backed up and the new GitLab istance must be the same.
+- [Restore GitLab](../raketasks/backup_restore.md#restore-gitlab).
+ Make sure to follow the instructions based on your installation method.
+ Confirm that the [secrets and configuration files](../raketasks/backup_restore.md#storing-configuration-files) are also restored.
+- If restoring from a snapshot, know the steps to do this.
+ **This process is out of scope for GitLab Support.**
+
+## Upgrade plan
+
+For the upgrade plan, start by creating an outline of a plan that best applies
+to your instance and then upgrade it for any relevant features you're using.
+
+- Generate an upgrade plan by reading and understanding the relevant documentation:
+ - upgrade based on the installation method:
+ - [Linux package (Omnibus)](index.md#linux-packages-omnibus-gitlab)
+ - [Compiled from source](index.md#installation-from-source)
+ - [Docker](index.md#installation-using-docker)
+ - [Helm Charts](index.md#installation-using-helm)
+ - [Zero-downtime updates](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates) ([if possible](index.md#upgrading-without-downtime) and desired)
+ - [Upgrade from GitLab Community Edition to Enterprise Edition, or vice-versa](https://docs.gitlab.com/omnibus/update/#upgrade-community-edition-to-enterprise-edition)
+- What version should you upgrade to:
+ - [Determine what upgrade path](index.md#upgrade-paths) to follow.
+ - Account for any [version-specific update instructions](index.md#version-specific-upgrading-instructions).
+ - Account for any [version-specific changes](https://docs.gitlab.com/omnibus/update/#version-specific-changes).
+ - Check the [OS compatibility with the target GitLab version](https://docs.gitlab.com/omnibus/package-information/deprecated_os.html).
+- Due to [background migrations](https://docs.gitlab.com/omnibus/update/#background-migrations),
+ plan to pause any further upgrades after updating to a new major version.
+ [All migrations must finish running](index.md#checking-for-background-migrations-before-upgrading)
+ before the next upgrade.
+- If available in your starting version, consider
+ [turning on maintenance mode](../administration/maintenance_mode/) during the
+ upgrade.
+- About PostgreSQL:
+ - On the top bar, select **Menu > Admin**, and look for the version of
+ PostgreSQL you are using.
+ If [a PostgreSQL upgrade is needed](https://docs.gitlab.com/omnibus/package-information/postgresql_versions.html),
+ account for the relevant
+ [packaged](https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server)
+ or [non-packaged](https://docs.gitlab.com/omnibus/settings/database.html#upgrade-a-non-packaged-postgresql-database) steps.
+
+### Additional features
+
+Apart from all the generic information above, you may have enabled some features
+that require special planning.
+
+Feel free to ignore sections about features that are inapplicable to your setup,
+such as Geo, external Gitaly, or Elasticsearch.
+
+#### External Gitaly
+
+If you're using an external Gitaly server, read the
+[upgrade Gitaly](https://docs.gitlab.com/omnibus/update/#upgrade-gitaly-servers)
+documentation.
+
+#### Geo
+
+If you're using Geo:
+
+- Review [Geo upgrade documentation](../administration/geo/replication/updating_the_geo_nodes.md).
+- Read about the [Geo version-specific update instructions](../administration/geo/replication/version_specific_updates.md).
+- Review Geo-specific steps when [updating the database](https://docs.gitlab.com/omnibus/settings/database.html#upgrading-a-geo-instance).
+- Create an upgrade and rollback plan for _each_ Geo node (primary and each secondary).
+
+#### Runners
+
+After updating GitLab, upgrade your runners to match
+[your new GitLab version](https://docs.gitlab.com/runner/#gitlab-runner-versions).
+
+#### Elasticsearch
+
+After updating GitLab, you may have to upgrade
+[Elasticsearch if the new version breaks compatibility](../integration/elasticsearch.md#version-requirements).
+Updating Elasticsearch is **out of scope for GitLab Support**.
+
+## Troubleshooting
+
+If anything doesn't go as planned:
+
+- If time is of the essence, copy any errors and gather any logs to later analyze,
+ and then [roll back to the last working version](#rollback-plan). You can use
+ the following tools to help you gather data:
+ - [`gitlabsos`](https://gitlab.com/gitlab-com/support/toolbox/gitlabsos) if
+ you installed GitLab using the Linux package or Docker.
+ - [`kubesos`](https://gitlab.com/gitlab-com/support/toolbox/kubesos/) if
+ you installed GitLab using the Helm Charts.
+- For support:
+ - [Contact GitLab Support](https://support.gitlab.com) and,
+ if you have one, your Technical Account Manager.
+ - If [the situation qualifies](https://about.gitlab.com/support/#definitions-of-support-impact)
+ and [your plan includes emergency support](https://about.gitlab.com/support/#priority-support),
+ create an emergency ticket.
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index ed6d49c2792..af4d32c703b 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -865,6 +865,18 @@ these steps:
echo -n "" > list_o_tags.out; for i in {1..N}; do curl --header 'PRIVATE-TOKEN: <PAT>' "https://gitlab.example.com/api/v4/projects/<Project_id>/registry/repositories/<container_repo_id>/tags?per_page=100&page=${i}" | jq '.[].name' | sed 's:^.\(.*\).$:\1:' >> list_o_tags.out; done
```
+ If you have Rails console access, you can enter the following commands to retrieve a list of tags limited by date:
+
+ ```shell
+ output = File.open( "/tmp/list_o_tags.out","w" )
+ Project.find(<Project_id>).container_repositories.find(<container_repo_id>).tags.each do |tag|
+ output << tag.name + "\n" if tag.created_at < 1.month.ago
+ end;nil
+ output.close
+ ```
+
+ This set of commands creates a `/tmp/list_o_tags.out` file listing all tags with a `created_at` date of older than one month.
+
1. Remove from the `list_o_tags.out` file any tags that you want to keep. Here are some example
`sed` commands for this. Note that these commands are simply examples. You may change them to
best suit your needs:
diff --git a/doc/user/packages/debian_repository/index.md b/doc/user/packages/debian_repository/index.md
index 789902c03e3..28489bb89b2 100644
--- a/doc/user/packages/debian_repository/index.md
+++ b/doc/user/packages/debian_repository/index.md
@@ -67,7 +67,7 @@ To create a distribution, publish a package, or install a private package, you n
following:
- [Personal access token](../../../api/index.md#personalproject-access-tokens)
-- [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token)
+- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
- [Deploy token](../../project/deploy_tokens/index.md)
## Create a Distribution
diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md
index aa6373b66cb..16b7a68d78e 100644
--- a/doc/user/packages/generic_packages/index.md
+++ b/doc/user/packages/generic_packages/index.md
@@ -21,13 +21,13 @@ Publish generic files, like release binaries, in your project's Package Registry
## Authenticate to the Package Registry
To authenticate to the Package Registry, you need either a [personal access token](../../../api/index.md#personalproject-access-tokens),
-[CI/CD job token](../../../api/index.md#gitlab-cicd-job-token), or [deploy token](../../project/deploy_tokens/index.md).
+[CI/CD job token](../../../ci/jobs/ci_job_token.md), or [deploy token](../../project/deploy_tokens/index.md).
In addition to the standard API authentication mechanisms, the generic package
API allows authentication with HTTP Basic authentication for use with tools that
do not support the other available mechanisms. The `user-id` is not checked and
may be any value, and the `password` must be either a [personal access token](../../../api/index.md#personalproject-access-tokens),
-a [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token), or a [deploy token](../../project/deploy_tokens/index.md).
+a [CI/CD job token](../../../ci/jobs/ci_job_token.md), or a [deploy token](../../project/deploy_tokens/index.md).
## Publish a package file
diff --git a/doc/user/packages/helm_repository/index.md b/doc/user/packages/helm_repository/index.md
index f98fc352ab5..6687fed525a 100644
--- a/doc/user/packages/helm_repository/index.md
+++ b/doc/user/packages/helm_repository/index.md
@@ -27,7 +27,7 @@ To authenticate to the Helm repository, you need either:
- A [personal access token](../../../api/index.md#personalproject-access-tokens).
- A [deploy token](../../project/deploy_tokens/index.md).
-- A [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token).
+- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
## Publish a package
diff --git a/doc/user/packages/terraform_module_registry/index.md b/doc/user/packages/terraform_module_registry/index.md
index b653ff553c2..6fc870425a8 100644
--- a/doc/user/packages/terraform_module_registry/index.md
+++ b/doc/user/packages/terraform_module_registry/index.md
@@ -16,7 +16,7 @@ as a Terraform module registry.
To authenticate to the Terraform module registry, you need either:
- A [personal access token](../../../api/index.md#personalproject-access-tokens) with at least `read_api` rights.
-- A [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token).
+- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
## Publish a Terraform Module
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 683496b4a9b..52b59d70302 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -103,6 +103,7 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/target_branch <local branch name>` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Set target branch. |
| `/title <new title>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Change title. |
| `/todo` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Add a to-do item. |
+| `/unapprove` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Unapprove the merge request. ([introduced in GitLab 14.3](https://gitlab.com/gitlab-org/gitlab/-/issues/8003)|
| `/unassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific assignees. |
| `/unassign` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all assignees. |
| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers. |
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 47c76e98e5c..6348a4902f8 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -155,6 +155,20 @@ module Gitlab
@execution_message[:approve] = _('Approved the current merge request.')
end
+ desc _('Unapprove a merge request')
+ explanation _('Unapprove the current merge request.')
+ types MergeRequest
+ condition do
+ quick_action_target.persisted? && quick_action_target.can_be_unapproved_by?(current_user)
+ end
+ command :unapprove do
+ success = MergeRequests::RemoveApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target)
+
+ next unless success
+
+ @execution_message[:unapprove] = _('Unapproved the current merge request.')
+ end
+
desc do
if quick_action_target.allows_multiple_reviewers?
_('Assign reviewer(s)')
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index 7df351859fb..7f77fa8ee02 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -3,6 +3,10 @@
category: quickactions
redis_slot: quickactions
aggregation: weekly
+- name: i_quickactions_unapprove
+ category: quickactions
+ redis_slot: quickactions
+ aggregation: weekly
- name: i_quickactions_assign_single
category: quickactions
redis_slot: quickactions
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ce956af6199..9de12fcc1eb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1723,6 +1723,9 @@ msgstr ""
msgid "Access granted"
msgstr ""
+msgid "Access key ID"
+msgstr ""
+
msgid "Access requests"
msgstr ""
@@ -12358,6 +12361,9 @@ msgstr ""
msgid "Enable"
msgstr ""
+msgid "Enable Amazon EKS integration"
+msgstr ""
+
msgid "Enable Auto DevOps"
msgstr ""
@@ -29486,6 +29492,9 @@ msgstr ""
msgid "Secret Detection"
msgstr ""
+msgid "Secret access key"
+msgstr ""
+
msgid "Secret token"
msgstr ""
@@ -35718,6 +35727,15 @@ msgstr ""
msgid "Unable to update this issue at this time."
msgstr ""
+msgid "Unapprove a merge request"
+msgstr ""
+
+msgid "Unapprove the current merge request."
+msgstr ""
+
+msgid "Unapproved the current merge request."
+msgstr ""
+
msgid "Unarchive project"
msgstr ""
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 274c2c2f3de..e48f59f6d9c 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,22 +1,24 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TableCellWrapper from '~/content_editor/components/wrappers/table_cell.vue';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
jest.mock('prosemirror-tables');
-describe('content/components/wrappers/table_cell', () => {
+describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
let editor;
let getPos;
- const createWrapper = async () => {
- wrapper = shallowMountExtended(TableCellWrapper, {
+ const createWrapper = async (propsData = { cellType: 'td' }) => {
+ wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
getPos,
+ ...propsData,
},
});
};
@@ -64,7 +66,7 @@ describe('content/components/wrappers/table_cell', () => {
setCurrentPositionInCell();
createWrapper();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdown().props()).toMatchObject({
category: 'tertiary',
@@ -81,7 +83,7 @@ describe('content/components/wrappers/table_cell', () => {
it('does not display dropdown when selection cursor is not on the cell', async () => {
createWrapper();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDropdown().exists()).toBe(false);
});
@@ -97,7 +99,7 @@ describe('content/components/wrappers/table_cell', () => {
});
createWrapper();
- await wrapper.vm.$nextTick();
+ await nextTick();
mockDropdownHide();
});
@@ -136,7 +138,7 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
- await wrapper.vm.$nextTick();
+ await nextTick();
findDropdownItemWithLabel('Delete row').vm.$emit('click');
@@ -154,11 +156,38 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
- await wrapper.vm.$nextTick();
+ await nextTick();
findDropdownItemWithLabel('Delete column').vm.$emit('click');
expect(mocks.deleteColumn).toHaveBeenCalled();
});
+
+ describe('when current row is the table’s header', () => {
+ beforeEach(async () => {
+ // Remove 2 rows condition
+ getSelectedRect.mockReturnValue({
+ map: {
+ height: 3,
+ },
+ });
+
+ createWrapper({ cellType: 'th' });
+
+ await nextTick();
+ });
+
+ it('does not allow adding a row before the header', async () => {
+ expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
+ });
+
+ it('does not allow removing the header row', async () => {
+ createWrapper({ cellType: 'th' });
+
+ await nextTick();
+
+ expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
new file mode 100644
index 00000000000..5d26c44ba03
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
+import TableCellBodyWrapper from '~/content_editor/components/wrappers/table_cell_body.vue';
+import { createTestEditor } from '../../test_utils';
+
+describe('content/components/wrappers/table_cell_body', () => {
+ let wrapper;
+ let editor;
+ let getPos;
+
+ const createWrapper = async () => {
+ wrapper = shallowMount(TableCellBodyWrapper, {
+ propsData: {
+ editor,
+ getPos,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ getPos = jest.fn();
+ editor = createTestEditor({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a TableCellBase component', () => {
+ createWrapper();
+ expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
+ editor,
+ getPos,
+ cellType: 'td',
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
new file mode 100644
index 00000000000..e561191418d
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
+import TableCellHeaderWrapper from '~/content_editor/components/wrappers/table_cell_header.vue';
+import { createTestEditor } from '../../test_utils';
+
+describe('content/components/wrappers/table_cell_header', () => {
+ let wrapper;
+ let editor;
+ let getPos;
+
+ const createWrapper = async () => {
+ wrapper = shallowMount(TableCellHeaderWrapper, {
+ propsData: {
+ editor,
+ getPos,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ getPos = jest.fn();
+ editor = createTestEditor({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a TableCellBase component', () => {
+ createWrapper();
+ expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
+ editor,
+ getPos,
+ cellType: 'th',
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index d462995328b..8331adcdfc2 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -375,6 +375,30 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('isBinary')).toBe(true);
},
);
+
+ it('passes the correct header props when viewing a non-text file', async () => {
+ fullFactory({
+ mockData: {
+ blobInfo: {
+ ...simpleMockData,
+ simpleViewer: {
+ ...simpleMockData.simpleViewer,
+ fileType: 'image',
+ },
+ },
+ },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
+ expect(findBlobHeader().props('isBinary')).toBe(true);
+ expect(findBlobEdit().props('showEditButton')).toBe(false);
+ });
});
describe('BlobButtonGroup', () => {
diff --git a/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js b/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js
new file mode 100644
index 00000000000..103eee4b9a8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js
@@ -0,0 +1,137 @@
+import { shallowMount } from '@vue/test-utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
+
+let data;
+let wrapper;
+
+function mountComponent({ rootStorageStatistics, limit }) {
+ wrapper = shallowMount(UsageGraph, {
+ propsData: {
+ rootStorageStatistics,
+ limit,
+ },
+ });
+}
+function findStorageTypeUsagesSerialized() {
+ return wrapper
+ .findAll('[data-testid="storage-type-usage"]')
+ .wrappers.map((wp) => wp.element.style.flex);
+}
+
+describe('Storage Counter usage graph component', () => {
+ beforeEach(() => {
+ data = {
+ rootStorageStatistics: {
+ wikiSize: 5000,
+ repositorySize: 4000,
+ packagesSize: 3000,
+ lfsObjectsSize: 2000,
+ buildArtifactsSize: 500,
+ pipelineArtifactsSize: 500,
+ snippetsSize: 2000,
+ storageSize: 17000,
+ uploadsSize: 1000,
+ },
+ limit: 2000,
+ };
+ mountComponent(data);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the legend in order', () => {
+ const types = wrapper.findAll('[data-testid="storage-type-legend"]');
+
+ const {
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = data.rootStorageStatistics;
+
+ expect(types.at(0).text()).toMatchInterpolatedText(`Wikis ${numberToHumanSize(wikiSize)}`);
+ expect(types.at(1).text()).toMatchInterpolatedText(
+ `Repositories ${numberToHumanSize(repositorySize)}`,
+ );
+ expect(types.at(2).text()).toMatchInterpolatedText(
+ `Packages ${numberToHumanSize(packagesSize)}`,
+ );
+ expect(types.at(3).text()).toMatchInterpolatedText(
+ `LFS Objects ${numberToHumanSize(lfsObjectsSize)}`,
+ );
+ expect(types.at(4).text()).toMatchInterpolatedText(
+ `Snippets ${numberToHumanSize(snippetsSize)}`,
+ );
+ expect(types.at(5).text()).toMatchInterpolatedText(
+ `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`,
+ );
+ expect(types.at(6).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`);
+ });
+
+ describe('when storage type is not used', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.wikiSize = 0;
+ mountComponent(data);
+ });
+
+ it('filters the storage type', () => {
+ expect(wrapper.text()).not.toContain('Wikis');
+ });
+ });
+
+ describe('when there is no storage usage', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.storageSize = 0;
+ mountComponent(data);
+ });
+
+ it('it does not render', () => {
+ expect(wrapper.html()).toEqual('');
+ });
+ });
+
+ describe('when limit is 0', () => {
+ beforeEach(() => {
+ data.limit = 0;
+ mountComponent(data);
+ });
+
+ it('sets correct flex values', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+
+ describe('when storage exceeds limit', () => {
+ beforeEach(() => {
+ data.limit = data.rootStorageStatistics.storageSize - 1;
+ mountComponent(data);
+ });
+
+ it('it does render correclty', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+});
diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_base_spec.rb
index c7ea2631a24..79053e98db7 100644
--- a/spec/models/concerns/approvable_base_spec.rb
+++ b/spec/models/concerns/approvable_base_spec.rb
@@ -60,6 +60,34 @@ RSpec.describe ApprovableBase do
end
end
+ describe '#can_be_unapproved_by?' do
+ subject { merge_request.can_be_unapproved_by?(user) }
+
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsy
+ end
+
+ context 'when a user has approved' do
+ let!(:approval) { create(:approval, merge_request: merge_request, user: user) }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when a user is nil' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ is_expected.to be_falsy
+ end
+ end
+ end
+
describe '.not_approved_by_users_with_usernames' do
subject { MergeRequest.not_approved_by_users_with_usernames([user.username, user2.username]) }
diff --git a/spec/services/pages/legacy_storage_lease_spec.rb b/spec/services/pages/legacy_storage_lease_spec.rb
deleted file mode 100644
index 092dce093ff..00000000000
--- a/spec/services/pages/legacy_storage_lease_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Pages::LegacyStorageLease do
- let(:project) { create(:project) }
-
- let(:implementation) do
- Class.new do
- include ::Pages::LegacyStorageLease
-
- attr_reader :project
-
- def initialize(project)
- @project = project
- end
-
- def execute
- try_obtain_lease do
- execute_unsafe
- end
- end
-
- def execute_unsafe
- true
- end
- end
- end
-
- let(:service) { implementation.new(project) }
-
- it 'allows method to be executed' do
- expect(service).to receive(:execute_unsafe).and_call_original
-
- expect(service.execute).to eq(true)
- end
-
- context 'when another service holds the lease for the same project' do
- around do |example|
- implementation.new(project).try_obtain_lease do
- example.run
- end
- end
-
- it 'does not run guarded method' do
- expect(service).not_to receive(:execute_unsafe)
-
- expect(service.execute).to eq(nil)
- end
- end
-
- context 'when another service holds the lease for the different project' do
- around do |example|
- implementation.new(create(:project)).try_obtain_lease do
- example.run
- end
- end
-
- it 'allows method to be executed' do
- expect(service).to receive(:execute_unsafe).and_call_original
-
- expect(service.execute).to eq(true)
- end
- end
-end
diff --git a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
index 25f571a73d1..177467aac85 100644
--- a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
+++ b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
@@ -114,13 +114,5 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
described_class.new(project).execute
end.not_to change { project.pages_metadatum.reload.pages_deployment_id }.from(old_deployment.id)
end
-
- it 'raises exception if exclusive lease is taken' do
- described_class.new(project).try_obtain_lease do
- expect do
- described_class.new(project).execute
- end.to raise_error(described_class::ExclusiveLeaseTakenError)
- end
- end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index a1b726071d6..02997096021 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -624,6 +624,18 @@ RSpec.describe QuickActions::InterpretService do
end
end
+ shared_examples 'approve command unavailable' do
+ it 'is not part of the available commands' do
+ expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :approve))
+ end
+ end
+
+ shared_examples 'unapprove command unavailable' do
+ it 'is not part of the available commands' do
+ expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :unapprove))
+ end
+ end
+
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _, _ = service.execute(content, issuable)
@@ -2135,6 +2147,66 @@ RSpec.describe QuickActions::InterpretService do
end
end
end
+
+ context 'approve command' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:content) { '/approve' }
+
+ it 'approves the current merge request' do
+ service.execute(content, merge_request)
+
+ expect(merge_request.approved_by_users).to eq([developer])
+ end
+
+ context "when the user can't approve" do
+ before do
+ project.team.truncate
+ project.add_guest(developer)
+ end
+
+ it 'does not approve the MR' do
+ service.execute(content, merge_request)
+
+ expect(merge_request.approved_by_users).to be_empty
+ end
+ end
+
+ it_behaves_like 'approve command unavailable' do
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'unapprove command' do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+ let(:content) { '/unapprove' }
+
+ before do
+ service.execute('/approve', merge_request)
+ end
+
+ it 'unapproves the current merge request' do
+ service.execute(content, merge_request)
+
+ expect(merge_request.approved_by_users).to be_empty
+ end
+
+ context "when the user can't unapprove" do
+ before do
+ project.team.truncate
+ project.add_guest(developer)
+ end
+
+ it 'does not unapprove the MR' do
+ service.execute(content, merge_request)
+
+ expect(merge_request.approved_by_users).to eq([developer])
+ end
+
+ it_behaves_like 'unapprove command unavailable' do
+ let(:issuable) { issue }
+ end
+ end
+ end
end
describe '#explain' do