diff options
85 files changed, 1396 insertions, 127 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index e8d433d36c9..2383e8747c3 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -83,6 +83,9 @@ const Api = { featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', billableGroupMembersPath: '/api/:version/groups/:id/billable_members', containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/', + projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings', + groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings', + notificationSettingsPath: '/api/:version/notification_settings', group(groupId, callback = () => {}) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -906,6 +909,34 @@ const Api = { return { data, headers }; }); }, + + async updateNotificationSettings(projectId, groupId, data = {}) { + let url = Api.buildUrl(this.notificationSettingsPath); + + if (projectId) { + url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId); + } else if (groupId) { + url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId); + } + + const result = await axios.put(url, data); + + return result; + }, + + async getNotificationSettings(projectId, groupId) { + let url = Api.buildUrl(this.notificationSettingsPath); + + if (projectId) { + url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId); + } else if (groupId) { + url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId); + } + + const result = await axios.get(url); + + return result; + }, }; export default Api; diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue new file mode 100644 index 00000000000..cb524fbffa2 --- /dev/null +++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue @@ -0,0 +1,169 @@ +<script> +import { + GlButtonGroup, + GlButton, + GlDropdown, + GlDropdownDivider, + GlTooltipDirective, +} from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import Api from '~/api'; +import NotificationsDropdownItem from './notifications_dropdown_item.vue'; +import { CUSTOM_LEVEL, i18n } from '../constants'; + +export default { + name: 'NotificationsDropdown', + components: { + GlButtonGroup, + GlButton, + GlDropdown, + GlDropdownDivider, + NotificationsDropdownItem, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: { + containerClass: { + default: '', + }, + disabled: { + default: false, + }, + dropdownItems: { + default: [], + }, + buttonSize: { + default: 'medium', + }, + initialNotificationLevel: { + default: '', + }, + projectId: { + default: null, + }, + groupId: { + default: null, + }, + }, + data() { + return { + selectedNotificationLevel: this.initialNotificationLevel, + isLoading: false, + }; + }, + computed: { + notificationLevels() { + return this.dropdownItems.map((level) => ({ + level, + title: this.$options.i18n.notificationTitles[level] || '', + description: this.$options.i18n.notificationDescriptions[level] || '', + })); + }, + isCustomNotification() { + return this.selectedNotificationLevel === CUSTOM_LEVEL; + }, + buttonIcon() { + if (this.isLoading) { + return null; + } + + return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications'; + }, + buttonTooltip() { + const notificationTitle = + this.$options.i18n.notificationTitles[this.selectedNotificationLevel] || + this.selectedNotificationLevel; + + return this.disabled + ? this.$options.i18n.notificationDescriptions.owner_disabled + : sprintf(this.$options.i18n.notificationTooltipTitle, { + notification_title: notificationTitle, + }); + }, + }, + methods: { + selectItem(level) { + if (level !== this.selectedNotificationLevel) { + this.updateNotificationLevel(level); + } + }, + async updateNotificationLevel(level) { + this.isLoading = true; + + try { + await Api.updateNotificationSettings(this.projectId, this.groupId, { level }); + this.selectedNotificationLevel = level; + } catch (error) { + this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' }); + } finally { + this.isLoading = false; + } + }, + }, + customLevel: CUSTOM_LEVEL, + i18n, +}; +</script> + +<template> + <div :class="containerClass"> + <gl-button-group + v-if="isCustomNotification" + v-gl-tooltip="{ title: buttonTooltip }" + data-testid="notificationButton" + :size="buttonSize" + > + <gl-button :size="buttonSize" :icon="buttonIcon" :loading="isLoading" :disabled="disabled" /> + <gl-dropdown :size="buttonSize" :disabled="disabled"> + <notifications-dropdown-item + v-for="item in notificationLevels" + :key="item.level" + :level="item.level" + :title="item.title" + :description="item.description" + :notification-level="selectedNotificationLevel" + @item-selected="selectItem" + /> + <gl-dropdown-divider /> + <notifications-dropdown-item + :key="$options.customLevel" + :level="$options.customLevel" + :title="$options.i18n.notificationTitles.custom" + :description="$options.i18n.notificationDescriptions.custom" + :notification-level="selectedNotificationLevel" + @item-selected="selectItem" + /> + </gl-dropdown> + </gl-button-group> + + <gl-dropdown + v-else + v-gl-tooltip="{ title: buttonTooltip }" + data-testid="notificationButton" + :icon="buttonIcon" + :loading="isLoading" + :size="buttonSize" + :disabled="disabled" + > + <notifications-dropdown-item + v-for="item in notificationLevels" + :key="item.level" + :level="item.level" + :title="item.title" + :description="item.description" + :notification-level="selectedNotificationLevel" + @item-selected="selectItem" + /> + <gl-dropdown-divider /> + <notifications-dropdown-item + :key="$options.customLevel" + :level="$options.customLevel" + :title="$options.i18n.notificationTitles.custom" + :description="$options.i18n.notificationDescriptions.custom" + :notification-level="selectedNotificationLevel" + @item-selected="selectItem" + /> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue new file mode 100644 index 00000000000..73bb9c1b36f --- /dev/null +++ b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue @@ -0,0 +1,42 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; + +export default { + name: 'NotificationsDropdownItem', + components: { + GlDropdownItem, + }, + props: { + level: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + description: { + type: String, + required: true, + }, + notificationLevel: { + type: String, + required: true, + }, + }, + computed: { + isActive() { + return this.notificationLevel === this.level; + }, + }, +}; +</script> + +<template> + <gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)"> + <div class="gl-display-flex gl-flex-direction-column"> + <span class="gl-font-weight-bold">{{ title }}</span> + <span class="gl-text-gray-500">{{ description }}</span> + </div> + </gl-dropdown-item> +</template> diff --git a/app/assets/javascripts/notifications/constants.js b/app/assets/javascripts/notifications/constants.js new file mode 100644 index 00000000000..f730ee59e84 --- /dev/null +++ b/app/assets/javascripts/notifications/constants.js @@ -0,0 +1,27 @@ +import { __, s__ } from '~/locale'; + +export const CUSTOM_LEVEL = 'custom'; + +export const i18n = { + notificationTitles: { + participating: s__('NotificationLevel|Participate'), + mention: s__('NotificationLevel|On mention'), + watch: s__('NotificationLevel|Watch'), + global: s__('NotificationLevel|Global'), + disabled: s__('NotificationLevel|Disabled'), + custom: s__('NotificationLevel|Custom'), + }, + notificationTooltipTitle: __('Notification setting - %{notification_title}'), + notificationDescriptions: { + participating: __('You will only receive notifications for threads you have participated in'), + mention: __('You will receive notifications only for comments in which you were @mentioned'), + watch: __('You will receive notifications for any activity'), + disabled: __('You will not get any notifications via email'), + global: __('Use your global notification setting'), + custom: __('You will only receive notifications for the events you choose'), + owner_disabled: __('Notifications have been disabled by the project or group owner'), + }, + updateNotificationLevelErrorMessage: __( + 'An error occured while updating the notification settings. Please try again.', + ), +}; diff --git a/app/assets/javascripts/notifications/index.js b/app/assets/javascripts/notifications/index.js new file mode 100644 index 00000000000..fe496c3aa3b --- /dev/null +++ b/app/assets/javascripts/notifications/index.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import NotificationsDropdown from './components/notifications_dropdown.vue'; + +Vue.use(GlToast); + +export default () => { + const el = document.querySelector('.js-vue-notification-dropdown'); + + if (!el) return false; + + const { + containerClass, + buttonSize, + disabled, + dropdownItems, + notificationLevel, + projectId, + groupId, + } = el.dataset; + + return new Vue({ + el, + provide: { + containerClass, + buttonSize, + disabled: parseBoolean(disabled), + dropdownItems: JSON.parse(dropdownItems), + initialNotificationLevel: notificationLevel, + projectId, + groupId, + }, + render(h) { + return h(NotificationsDropdown); + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index cc676b98e49..a2eb286076a 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -12,6 +12,7 @@ import notificationsDropdown from '../../../notifications_dropdown'; import { showLearnGitLabProjectPopover } from '~/onboarding_issues'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initVueNotificationsDropdown from '~/notifications'; initReadMore(); new Star(); // eslint-disable-line no-new @@ -42,7 +43,14 @@ leaveByUrl('project'); showLearnGitLabProjectPopover(); -notificationsDropdown(); +if (gon.features?.vueNotificationDropdown) { + initVueNotificationsDropdown(); +} else { + notificationsDropdown(); +} + +initVueNotificationsDropdown(); + new ShortcutsNavigation(); // eslint-disable-line no-new initInviteMembersTrigger(); diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index eef54ee561f..64cb1c1ee52 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -31,6 +31,10 @@ class ProjectsController < Projects::ApplicationController # Project Export Rate Limit before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export] + before_action do + push_frontend_feature_flag(:vue_notification_dropdown, @project, default_enabled: :yaml) + end + before_action only: [:edit] do push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true) push_frontend_feature_flag(:allow_editing_commit_messages, @project) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 2b68d953431..729585be84a 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -125,4 +125,13 @@ module NotificationsHelper def can_read_project?(project) can?(current_user, :read_project, project) end + + def notification_dropdown_items(notification_setting) + NotificationSetting.levels.each_key.map do |level| + next if level == "custom" + next if level == "global" && notification_setting.source.nil? + + level + end.compact + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 03cb53f55be..f6d1b376b92 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -30,6 +30,9 @@ class ProjectPolicy < BasePolicy desc "User has maintainer access" condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER } + desc "User is a project bot" + condition(:project_bot) { user.project_bot? && team_member? } + desc "Project is public" condition(:public_project, scope: :subject, score: 0) { project.public? } @@ -616,10 +619,14 @@ class ProjectPolicy < BasePolicy prevent :read_project end + rule { project_bot }.enable :project_bot_access + rule { resource_access_token_available & can?(:admin_project) }.policy do enable :admin_resource_access_tokens end + rule { can?(:project_bot_access) }.prevent :admin_resource_access_tokens + rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do enable :set_pipeline_variables end diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb index 76da4693415..fea7fc55d90 100644 --- a/app/services/users/approve_service.rb +++ b/app/services/users/approve_service.rb @@ -17,6 +17,7 @@ module Users user.accept_pending_invitations! if user.active_for_authentication? DeviseMailer.user_admin_approval(user).deliver_later + log_event(user) after_approve_hook(user) success(message: 'Success', http_status: :created) else @@ -39,6 +40,10 @@ module Users def approval_required?(user) user.blocked_pending_approval? end + + def log_event(user) + Gitlab::AppLogger.info(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end end end diff --git a/app/services/users/reject_service.rb b/app/services/users/reject_service.rb index 1a103c77262..0e3eb3e5dde 100644 --- a/app/services/users/reject_service.rb +++ b/app/services/users/reject_service.rb @@ -16,6 +16,8 @@ module Users NotificationService.new.user_admin_rejection(user.name, user.email) + log_event(user) + success end @@ -30,6 +32,10 @@ module Users def after_reject_hook(user) # overridden by EE module end + + def log_event(user) + Gitlab::AppLogger.info(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end end end diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1f826567002..9414e9b32d5 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -46,7 +46,11 @@ .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .d-inline-flex - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled + - if Feature.enabled?(:vue_notification_dropdown, @project, default_enabled: :yaml) + - if @notification_setting + .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, container_class: 'gl-mr-3 gl-mt-5 gl-vertical-align-top' } } + - else + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled .count-buttons.d-inline-flex = render 'projects/buttons/star' diff --git a/changelogs/unreleased/298783-log-user-instance-request-in-audit-events.yml b/changelogs/unreleased/298783-log-user-instance-request-in-audit-events.yml new file mode 100644 index 00000000000..21383599b5b --- /dev/null +++ b/changelogs/unreleased/298783-log-user-instance-request-in-audit-events.yml @@ -0,0 +1,5 @@ +--- +title: Log user approval/rejection in application logs +merge_request: 51768 +author: +type: added diff --git a/changelogs/unreleased/sfang-project-access-token-api.yml b/changelogs/unreleased/sfang-project-access-token-api.yml new file mode 100644 index 00000000000..96de9cea3e5 --- /dev/null +++ b/changelogs/unreleased/sfang-project-access-token-api.yml @@ -0,0 +1,5 @@ +--- +title: Project access token management via API +merge_request: 52139 +author: +type: added diff --git a/config/feature_flags/development/vue_notification_dropdown.yml b/config/feature_flags/development/vue_notification_dropdown.yml new file mode 100644 index 00000000000..4c4f67fe928 --- /dev/null +++ b/config/feature_flags/development/vue_notification_dropdown.yml @@ -0,0 +1,8 @@ +--- +name: vue_notification_dropdown +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52068 +rollout_issue_url: +milestone: '13.8' +type: development +group: group::optimize +default_enabled: false diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index 1e379236395..a81895bc0f8 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -12,6 +12,7 @@ Ansible Anthos approvers architected +architecting Arel Artifactory Asana @@ -60,6 +61,10 @@ boolean booleans Bootsnap browsable +bugfix +bugfixed +bugfixes +bugfixing Bugzilla Buildkite buildpack @@ -353,6 +358,7 @@ onboarding OpenID OpenShift Opsgenie +Overcommit Packagist parallelization parallelizations @@ -404,6 +410,7 @@ pseudocode pseudonymized pseudonymizer Puma +pytest Python Qualys queryable @@ -434,6 +441,8 @@ referer referers reflog reflogs +refspec +refspecs reindex reindexed reindexes @@ -499,6 +508,7 @@ Silverlight Sisense Sitespeed Slack +Slackbot Slony smartcard smartcards @@ -541,11 +551,15 @@ subquery subquerying substring substrings +subtask +subtasks subtree subtrees sudo supercookie supercookies +supertype +supertypes swappiness swimlane swimlanes @@ -668,6 +682,7 @@ unverify unverifying uploader uploaders +upstreams upvoted upvotes URIs diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md index 61dfefee813..b2c8d8982bd 100644 --- a/doc/administration/compliance.md +++ b/doc/administration/compliance.md @@ -15,9 +15,9 @@ relevant compliance standards. |Feature |GitLab tier |GitLab.com | Product level | | ---------| :--------: | :-------: | :-----------: | -|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Core+||Instance| -|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Core+|✓|Instance, Group, Project| -|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Core+||Instance| +|**[Restrict SSH Keys](../security/ssh_keys_restrictions.md)**<br>Control the technology and key length of SSH keys used to access GitLab|Free+||Instance| +|**[Granular user roles and flexible permissions](../user/permissions.md)**<br>Manage access and permissions with five different user roles and settings for external users. Set permissions according to people's role, rather than either read or write access to a repository. Don't share the source code with people that only need access to the issue tracker.|Free+|✓|Instance, Group, Project| +|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Free+||Instance| |**[Email all users of a project, group, or entire server](../tools/email.md)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+|||Instance |**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+||Instance| |**[Lock project membership to group](../user/group/index.md#member-lock)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|Group| diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 8016a572da2..349204e87f2 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -10,7 +10,7 @@ type: reference [Gitaly](index.md), the service that provides storage for Git repositories, can be run in a clustered configuration to increase fault tolerance. In this configuration, every Git repository is stored on every Gitaly node in the -cluster. Multiple clusters (or shards) can be configured. +cluster. Multiple clusters (or storage shards) can be configured. NOTE: Technical support for Gitaly clusters is limited to GitLab Premium and Ultimate @@ -21,7 +21,7 @@ component for running a Gitaly Cluster. ![Architecture diagram](img/praefect_architecture_v12_10.png) -Using a Gitaly Cluster increase fault tolerance by: +Using a Gitaly Cluster increases fault tolerance by: - Replicating write operations to warm standby Gitaly nodes. - Detecting Gitaly node failures. @@ -53,7 +53,7 @@ Gitaly Cluster supports: - Reporting of possible data loss if replication queue is non-empty. - Marking repositories as [read only](#read-only-mode) if data loss is detected to prevent data inconsistencies. -Follow the [HA Gitaly epic](https://gitlab.com/groups/gitlab-org/-/epics/1489) +Follow the [Gitaly Cluster epic](https://gitlab.com/groups/gitlab-org/-/epics/1489) for improvements including [horizontally distributing reads](https://gitlab.com/groups/gitlab-org/-/epics/2013). @@ -80,23 +80,65 @@ For more information, see: - [Gitaly architecture](index.md#architecture). - Geo [use cases](../geo/index.md#use-cases) and [architecture](../geo/index.md#architecture). -## Cluster or shard +## Where Gitaly Cluster fits + +GitLab accesses [repositories](../../user/project/repository/index.md) through the configured +[repository storages](../repository_storage_paths.md). Each new repository is stored on one of the +repository storages based on their configured weights. Each repository storage is either: + +- A Gitaly storage served directly by Gitaly. These map to a directory on the file system of a + Gitaly node. +- A [virtual storage](#virtual-storage-or-direct-gitaly-storage) served by Praefect. A virtual + storage is a cluster of Gitaly storages that appear as a single repository storage. + +Virtual storages are a feature of Gitaly Cluster. They support replicating the repositories to +multiple storages for fault tolerance. Virtual storages can improve performance by distributing +requests across Gitaly nodes. Their distributed nature makes it viable to have a single repository +storage in GitLab to simplify repository management. + +## Components of Gitaly Cluster + +Gitaly Cluster consists of multiple components: + +- [Load balancer](#load-balancer) for distributing requests and providing fault-tolerant access to + Praefect nodes. +- [Praefect](#praefect) nodes for managing the cluster and routing requests to Gitaly nodes. +- [PostgreSQL database](#postgresql) for persisting cluster metadata and [PgBouncer](#pgbouncer), + recommended for pooling Praefect's database connections. +- [Gitaly](index.md) nodes to provide repository storage and Git access. + +![Cluster example](img/cluster_example_v13_3.png) + +In this example: + +- Repositories are stored on a virtual storage called `storage-1`. +- Three Gitaly nodes provide `storage-1` access: `gitaly-1`, `gitaly-2`, and `gitaly-3`. +- The three Gitaly nodes store data on their filesystems. + +### Virtual storage or direct Gitaly storage Gitaly supports multiple models of scaling: - Clustering using Gitaly Cluster, where each repository is stored on multiple Gitaly nodes in the cluster. Read requests are distributed between repository replicas and write requests are - broadcast to repository replicas. -- Sharding using [repository storage paths](../repository_storage_paths.md), where each repository - is stored on the assigned Gitaly node. All requests are routed to this node. + broadcast to repository replicas. GitLab accesses virtual storage. +- Direct access to Gitaly storage using [repository storage paths](../repository_storage_paths.md), + where each repository is stored on the assigned Gitaly node. All requests are routed to this node. + +The following is Gitaly set up to use direct access to Gitaly instead of Gitaly Cluster: + +![Shard example](img/shard_example_v13_3.png) + +In this example: -| Cluster | Shard | -|:--------------------------------------------------|:----------------------------------------------| -| ![Cluster example](img/cluster_example_v13_3.png) | ![Shard example](img/shard_example_v13_3.png) | +- Each repository is stored on one of three Gitaly storages: `storage-1`, `storage-2`, + or `storage-3`. +- Each storage is serviced by a Gitaly node. +- The three Gitaly nodes store data in three separate hashed storage locations. -Generally, Gitaly Cluster can replace sharded configurations, at the expense of additional storage -needed to store each repository on multiple Gitaly nodes. The benefit of using Gitaly Cluster over -sharding is: +Generally, virtual storage with Gitaly Cluster can replace direct Gitaly storage configurations, at +the expense of additional storage needed to store each repository on multiple Gitaly nodes. The +benefit of using Gitaly Cluster over direct Gitaly storage is: - Improved fault tolerance, because each Gitaly node has a copy of every repository. - Improved resource utilization, reducing the need for over-provisioning for shard-specific peak @@ -773,7 +815,7 @@ configuration. ### Load Balancer -In a highly available Gitaly configuration, a load balancer is needed to route +In a fault-tolerant Gitaly configuration, a load balancer is needed to route internal traffic from the GitLab application to the Praefect nodes. The specifics on which load balancer to use or the exact configuration is beyond the scope of the GitLab documentation. @@ -785,7 +827,7 @@ addition to the GitLab nodes. Some requests handled by process. `gitaly-ruby` uses the Gitaly address set in the GitLab server's `git_data_dirs` setting to make this connection. -We hope that if you’re managing HA systems like GitLab, you have a load balancer +We hope that if you’re managing fault-tolerant systems like GitLab, you have a load balancer of choice already. Some examples include [HAProxy](https://www.haproxy.org/) (open-source), [Google Internal Load Balancer](https://cloud.google.com/load-balancing/docs/internal/), [AWS Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/), F5 @@ -974,7 +1016,7 @@ To get started quickly: 1. Go to **Explore** and query `gitlab_build_info` to verify that you are getting metrics from all your machines. -Congratulations! You've configured an observable highly available Praefect +Congratulations! You've configured an observable fault-tolerant Praefect cluster. ## Distributed reads diff --git a/doc/administration/index.md b/doc/administration/index.md index e1da47bfec7..190f4dca619 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -17,7 +17,7 @@ GitLab has two product distributions available through [different subscriptions] You can [install either GitLab CE or GitLab EE](https://about.gitlab.com/install/ce-or-ee/). However, the features you have access to depend on your chosen [subscription](https://about.gitlab.com/pricing/). -GitLab Community Edition installations have access only to Core features. +GitLab Community Edition installations have access only to Free features. Non-administrator users can't access GitLab administration tools and settings. diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md index 8f33ddf3ca5..56a305fa4da 100644 --- a/doc/administration/instance_review.md +++ b/doc/administration/instance_review.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Instance Review -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Core](https://about.gitlab.com/pricing/) 11.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Free](https://about.gitlab.com/pricing/) 11.3. If you run a medium-sized self-managed instance (50+ users) of a free version of GitLab, [either Community Edition or unlicensed Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/), diff --git a/doc/administration/integration/kroki.md b/doc/administration/integration/kroki.md index 8bf1b18131b..5e458399773 100644 --- a/doc/administration/integration/kroki.md +++ b/doc/administration/integration/kroki.md @@ -1,3 +1,9 @@ +--- +stage: Create +group: Ecosystem +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 +--- + # Kroki diagrams **(FREE SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241744) in GitLab 13.7. diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 3f6545824c8..ba2cb05b449 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -91,7 +91,7 @@ _The artifacts are stored by default in > [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. > - Since version 9.5, artifacts are [browsable](../ci/pipelines/job_artifacts.md#browsing-artifacts), > when object storage is enabled. 9.4 lacks this feature. -> - Since version 10.6, available in [GitLab Core](https://about.gitlab.com/pricing/) +> - Since version 10.6, available in [GitLab Free](https://about.gitlab.com/pricing/). > - Since version 11.0, we support `direct_upload` to S3. If you don't want to use the local disk where GitLab is installed to store the diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md index 67579deb813..0157ea8f430 100644 --- a/doc/administration/operations/extra_sidekiq_processes.md +++ b/doc/administration/operations/extra_sidekiq_processes.md @@ -27,7 +27,7 @@ can be started. ## Start multiple processes > - [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4006) in GitLab 12.10, starting multiple processes with Sidekiq cluster. -> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10. +> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10. > - [Sidekiq cluster became default](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4140) in GitLab 13.0. To start multiple processes: @@ -113,7 +113,7 @@ you list: ## Queue selector > - [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8. -> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10. +> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Free](https://about.gitlab.com/pricing/) in GitLab 12.10. > - [Renamed from `experimental_queue_selector` to `queue_selector`](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/147) in GitLab 13.6. In addition to selecting queues by name, as above, the `queue_selector` diff --git a/doc/administration/packages/dependency_proxy.md b/doc/administration/packages/dependency_proxy.md index 9e315722c24..16cfdc8c784 100644 --- a/doc/administration/packages/dependency_proxy.md +++ b/doc/administration/packages/dependency_proxy.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # GitLab Dependency Proxy administration > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Free](https://about.gitlab.com/pricing/) in GitLab 13.6. GitLab can be used as a dependency proxy for a variety of common package managers. diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index da63cf32cd9..52e4db6ca31 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -161,7 +161,8 @@ When avatar is replaced, `Upload` model is destroyed and a new one takes place w #### CI artifacts -CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Core since **10.6**. +CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Free since +**10.6**. #### LFS objects diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md index 07a7baf338b..26e44a49aec 100644 --- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md +++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md @@ -70,7 +70,7 @@ and they will assist you with any issues you are having. kubectl logs <pod-name> --previous ``` - No logs are kept in the containers/pods themselves. Everything is written to stdout. + No logs are kept in the containers/pods themselves. Everything is written to `stdout`. This is the principle of Kubernetes, read [Twelve-factor app](https://12factor.net/) for details. diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 3ce5ef2b8af..d3a9777775f 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -54,7 +54,7 @@ _The uploads are stored by default in > **Notes:** > > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Core](https://about.gitlab.com/pricing/) 10.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Free](https://about.gitlab.com/pricing/) 10.7. > - Since version 11.1, we support direct_upload to S3. If you don't want to use the local disk where GitLab is installed to store the diff --git a/doc/api/README.md b/doc/api/README.md index e2ec30ce8ad..da8a788bd80 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -124,7 +124,7 @@ There are several methods you can use to authenticate with the GitLab API: - [GitLab CI/CD job token](#gitlab-ci-job-token) **(Specific endpoints only)** NOTE: -Project access tokens are supported for self-managed instances on Core and +Project access tokens are supported for self-managed instances on Free and higher. They're also supported on GitLab.com Bronze and higher. For administrators who want to authenticate with the API as a specific user, or who want diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 5f129ccf848..7bb0c4ec67e 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -25,6 +25,7 @@ The following API resources are available in the project context: | Resource | Available endpoints | |:--------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | +| [Access tokens](resource_access_tokens.md) | `/projects/:id/access_tokens` | | [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` | | [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` | | [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | @@ -76,7 +77,7 @@ The following API resources are available in the project context: | [Remote mirrors](remote_mirrors.md) | `/projects/:id/remote_mirrors` | | [Repositories](repositories.md) | `/projects/:id/repository` | | [Repository files](repository_files.md) | `/projects/:id/repository/files` | -| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` | +| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` | | [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` (also available for groups) | | [Runners](runners.md) | `/projects/:id/runners` (also available standalone) | | [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) | diff --git a/doc/api/dependency_proxy.md b/doc/api/dependency_proxy.md index ab691aaecea..c1745e4e0bb 100644 --- a/doc/api/dependency_proxy.md +++ b/doc/api/dependency_proxy.md @@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Purge the dependency proxy for a group > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11631) in GitLab 12.10. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Free](https://about.gitlab.com/pricing/) in GitLab 13.6. Deletes the cached manifests and blobs for a group. This endpoint requires group admin access. diff --git a/doc/api/feature_flag_user_lists.md b/doc/api/feature_flag_user_lists.md index e838ee1b22f..e0a5fe99663 100644 --- a/doc/api/feature_flag_user_lists.md +++ b/doc/api/feature_flag_user_lists.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Feature flag user lists API **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205409) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to GitLab Core in 13.5. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to GitLab Free in 13.5. API for accessing GitLab Feature Flag User Lists. diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md index ab631332d9e..77d30475555 100644 --- a/doc/api/feature_flags.md +++ b/doc/api/feature_flags.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.5. API for accessing resources of [GitLab Feature Flags](../operations/feature_flags.md). diff --git a/doc/api/feature_flags_legacy.md b/doc/api/feature_flags_legacy.md index e4c17486908..33ea2727fb9 100644 --- a/doc/api/feature_flags_legacy.md +++ b/doc/api/feature_flags_legacy.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.5. WARNING: This API is deprecated and [scheduled for removal in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213369). Use [this API](feature_flags.md) instead. diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md index f05b23495bb..2761c1a1c84 100644 --- a/doc/api/graphql/removed_items.md +++ b/doc/api/graphql/removed_items.md @@ -1,3 +1,9 @@ +--- +stage: Plan +group: Product Planning +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 +--- + # GraphQL API removed items GraphQL is a versionless API, unlike the REST API. diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md index a01b8d22611..0d55b77df0e 100644 --- a/doc/api/issue_links.md +++ b/doc/api/issue_links.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Issue links API **(FREE)** -> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.4. +> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.4. ## List issue relations diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md index ca0ac3522c3..4949bf504fa 100644 --- a/doc/api/personal_access_tokens.md +++ b/doc/api/personal_access_tokens.md @@ -11,7 +11,7 @@ You can read more about [personal access tokens](../user/profile/personal_access ## List personal access tokens > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227264) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.6. Get a list of personal access tokens. @@ -71,7 +71,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a ## Revoke a personal access token > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Free](https://about.gitlab.com/pricing/) in 13.6. Revoke a personal access token. diff --git a/doc/api/resource_access_tokens.md b/doc/api/resource_access_tokens.md new file mode 100644 index 00000000000..44dfc243750 --- /dev/null +++ b/doc/api/resource_access_tokens.md @@ -0,0 +1,107 @@ +--- +stage: Manage +group: Access +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 +--- + +# Project access tokens API + +You can read more about [project access tokens](../user/project/settings/project_access_tokens.md). + +## List project access tokens + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9. + +Get a list of project access tokens. + +```plaintext +GET /:id/access_tokens +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer/string | yes | The ID of the project | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens" +``` + +```json +[ + { + "user_id" : 141, + "scopes" : [ + "api" + ], + "name" : "token", + "expires_at" : "2021-01-31", + "id" : 42, + "active" : true, + "created_at" : "2021-01-20T22:11:48.151Z", + "revoked" : false + } +] +``` + +## Create a project access token + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9. + +Create a project access token. + +```plaintext +POST /:id/access_tokens +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `name` | String | yes | The name of the project access token | +| `scopes` | Array[String] | yes | [List of scopes](../user/project/settings/project_access_tokens.md#limiting-scopes-of-a-project-access-token) | +| `expires_at` | Date | no | The token expires at midnight UTC on that date | + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ +--header "Content-Type:application/json" \ +--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31" }' \ +"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens" +``` + +```json +{ + "scopes" : [ + "api", + "read_repository" + ], + "active" : true, + "name" : "test", + "revoked" : false, + "created_at" : "2021-01-21T19:35:37.921Z", + "user_id" : 166, + "id" : 58, + "expires_at" : "2021-01-31" +} +``` + +## Revoke a project access token + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238991) in GitLab 13.9. + +Revoke a project access token. + +```plaintext +DELETE /:id/access_tokens/:token_id +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer/string | yes | The ID of the project | +| `token_id` | integer/string | yes | The ID of the project access token | + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens/<token_id>" +``` + +### Responses + +- `204: No Content` if successfully revoked. +- `400 Bad Request` or `404 Not Found` if not revoked successfully. diff --git a/doc/ci/README.md b/doc/ci/README.md index 2327f1d41d3..dcac628eab1 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -71,7 +71,7 @@ available through the UI. You can use them by creating a new file, choosing a template that suits your application, and adjusting it to your needs: -![Use a `.gitlab-ci.yml` template](img/add_file_template_11_10.png) +![Use a YAML template](img/add_file_template_11_10.png) While building your `.gitlab-ci.yml`, you can use the [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration) to facilitate your writing experience. diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md index 7a3f2fd4313..4ec921b4447 100644 --- a/doc/ci/chatops/README.md +++ b/doc/ci/chatops/README.md @@ -5,10 +5,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: index, concepts, howto --- -# GitLab ChatOps **(CORE)** +# GitLab ChatOps **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. -> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24780) to [GitLab Free](https://about.gitlab.com/pricing/) in 11.9. GitLab ChatOps provides a method to interact with CI/CD jobs through chat services like Slack. Many organizations' discussion, collaboration, and troubleshooting takes diff --git a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md index a466214374b..b7418f08bf5 100644 --- a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md +++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md @@ -14,8 +14,9 @@ GitLab CI/CD can be used with Bitbucket Cloud by: To use GitLab CI/CD with a Bitbucket Cloud repository: -1. In GitLab create a **CI/CD for external repository**, select **Repo by URL** and - create the project. +1. <!-- vale gitlab.Spelling = NO --> In GitLab create a **CI/CD for external repository**, select + **Repo by URL** and create the project. + <!-- vale gitlab.Spelling = YES --> ![Create project](img/external_repository.png) diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md index 44534ddf793..cc6c629fb47 100644 --- a/doc/ci/ci_cd_for_external_repos/index.md +++ b/doc/ci/ci_cd_for_external_repos/index.md @@ -25,11 +25,15 @@ snippets disabled. These features To connect to an external repository: +<!-- vale gitlab.Spelling = NO --> + 1. From your GitLab dashboard, click **New project**. 1. Switch to the **CI/CD for external repository** tab. 1. Choose **GitHub** or **Repo by URL**. 1. The next steps are similar to the [import flow](../../user/project/import/index.md). +<!-- vale gitlab.Spelling = YES --> + ![CI/CD for external repository project creation](img/ci_cd_for_external_repo.png) ## Pipelines for external pull requests diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index af88a006156..5506676a8b8 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -126,7 +126,7 @@ not without its own challenges: instance of Docker engine so they don't conflict with each other. But this also means that jobs can be slower because there's no caching of layers. - By default, Docker 17.09 and higher uses `--storage-driver overlay2` which is - the recommended storage driver. See [Using the overlayfs driver](#use-the-overlayfs-driver) + the recommended storage driver. See [Using the OverlayFS driver](#use-the-overlayfs-driver) for details. - Since the `docker:19.03.12-dind` container and the runner container don't share their root file system, the job's working directory can be used as a mount point for diff --git a/doc/ci/environments/environments_dashboard.md b/doc/ci/environments/environments_dashboard.md index 5af83068431..ef222ba5779 100644 --- a/doc/ci/environments/environments_dashboard.md +++ b/doc/ci/environments/environments_dashboard.md @@ -36,7 +36,7 @@ environments are not displayed. To add a project to the dashboard: -1. Click the **Add projects** button in the homescreen of the dashboard. +1. Click the **Add projects** button in the home screen of the dashboard. 1. Search and add one or more projects using the **Search your projects** field. 1. Click the **Add projects** button. diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index 7bf30ef1b95..58e12fdd733 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -1009,7 +1009,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ### Scoping environments with specs > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. -> - [Scoping for environment variables was moved to Core](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2. +> - [Scoping for environment variables was moved to Free](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2. You can limit the environment scope of a variable by defining which environments it can be available for. diff --git a/doc/ci/lint.md b/doc/ci/lint.md index ec3c128f076..969f1fbc47b 100644 --- a/doc/ci/lint.md +++ b/doc/ci/lint.md @@ -4,8 +4,10 @@ group: unassigned 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 --- <!-- markdownlint-disable MD044 --> +<!-- vale gitlab.Spelling = NO --> # Validate .gitlab-ci.yml syntax with the CI Lint tool <!-- markdownlint-enable MD044 --> +<!-- vale gitlab.Spelling = YES --> If you want to test the validity of your GitLab CI/CD configuration before committing the changes, you can use the CI Lint tool. This tool checks for syntax and logical diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md index 006d6bda0e0..a84fb3fa06f 100644 --- a/doc/ci/multi_project_pipelines.md +++ b/doc/ci/multi_project_pipelines.md @@ -261,7 +261,7 @@ test: ### Mirroring status from triggered pipeline > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Core in 12.8. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. You can mirror the pipeline status from the triggered pipeline to the source bridge job by using `strategy: depend`. For example: diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md index d088d0d7e4d..58b7e303138 100644 --- a/doc/ci/parent_child_pipelines.md +++ b/doc/ci/parent_child_pipelines.md @@ -156,7 +156,11 @@ build a matrix of targets and architectures. <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM). -We also have an [example project using Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet) which shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. You could use a similar process for other templating languages like [Dhall](https://dhall-lang.org/) or [`ytt`](https://get-ytt.io/). +<!-- vale gitlab.Spelling = NO --> +We also have an example project using +[Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet) +which shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. You could use a similar process for other templating languages like [Dhall](https://dhall-lang.org/) or [`ytt`](https://get-ytt.io/). +<!-- vale gitlab.Spelling = NO --> The artifact path is parsed by GitLab, not the runner, so the path must match the syntax for the OS running GitLab. If GitLab is running on Linux but using a Windows diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 32cab3b55f7..20916588962 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -131,6 +131,8 @@ averaged. ![Build status coverage](img/pipelines_test_coverage_build.png) +<!-- vale gitlab.Spelling = NO --> + | Coverage Tool | Sample regular expression | |------------------------------------------------|---------------------------| | Simplecov (Ruby) | `\(\d+.\d+\%\) covered` | @@ -145,6 +147,8 @@ averaged. | JaCoCo (Java/Kotlin) | `Total.*?([0-9]{1,3})%` | | `go test -cover` (Go) | `coverage: \d+.\d+% of statements` | +<!-- vale gitlab.Spelling = YES --> + ### Code Coverage history > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10. @@ -154,7 +158,7 @@ To see the evolution of your project code coverage over time, you can view a graph or download a CSV file with this data. From your project: 1. Go to **{chart}** **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph. -1. If you want a CSV file of that data, click **Download raw data (.csv)** +1. If you want a CSV file of that data, click **Download raw data (`.csv`)** ![Code coverage graph of a project over time](img/code_coverage_graph_v13_1.png) diff --git a/doc/ci/unit_test_reports.md b/doc/ci/unit_test_reports.md index 1d774fa4135..e72d746c42b 100644 --- a/doc/ci/unit_test_reports.md +++ b/doc/ci/unit_test_reports.md @@ -303,7 +303,7 @@ test: artifacts: when: always reports: - junit: + junit: - report.xml ``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index b7d5f64c0a6..57c595360b6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -341,7 +341,7 @@ include: > - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. > - Available for Starter, Premium, and Ultimate in GitLab 10.6 and later. -> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Core in 11.4. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4. Use the `include` keyword to include external YAML files in your CI/CD configuration. You can break down one long `gitlab-ci.yml` file into multiple files to increase readability, @@ -3675,7 +3675,7 @@ deploystacks: ### `trigger` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Core in 12.8. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. Use `trigger` to define a downstream pipeline trigger. When GitLab starts a `trigger` job, a downstream pipeline is created. diff --git a/doc/development/adding_service_component.md b/doc/development/adding_service_component.md index 7e2add2c91f..f3e23906ac6 100644 --- a/doc/development/adding_service_component.md +++ b/doc/development/adding_service_component.md @@ -65,7 +65,7 @@ Notify the [Distribution team](https://about.gitlab.com/handbook/engineering/dev New services to be bundled with GitLab need to be available in the following environments. -**Dev environment** +**Development environment** The first step of bundling a new service is to provide it in the development environment to engage in collaboration and feedback. diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index a6968a7da1d..c0317a7e73e 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -1683,7 +1683,7 @@ full stack: - An argument or scalar's [`prepare`](#validating-arguments) applies correctly. - Logic in a resolver or mutation's [`#ready?` method](#correct-use-of-resolverready) applies correctly. - An [argument's `default_value`](https://graphql-ruby.org/fields/arguments.html) applies correctly. -- Objects resolve performantly and there are no N+1 issues. +- Objects resolve successfully, and there are no N+1 issues. When adding a query, you can use the `a working graphql query` shared example to test if the query renders valid results. diff --git a/doc/development/architecture.md b/doc/development/architecture.md index f8ab97de848..d8b0781da78 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -934,7 +934,7 @@ ps aux | grep '^git' ``` GitLab has several components to operate. It requires a persistent database -(PostgreSQL) and Redis database, and uses Apache `httpd` or NGINX to proxypass +(PostgreSQL) and Redis database, and uses Apache `httpd` or NGINX to `proxypass` Puma. All these components should run as different system users to GitLab (for example, `postgres`, `redis`, and `www-data`, instead of `git`). diff --git a/doc/development/chaos_endpoints.md b/doc/development/chaos_endpoints.md index ccb7b9863d0..85c93f521ac 100644 --- a/doc/development/chaos_endpoints.md +++ b/doc/development/chaos_endpoints.md @@ -6,8 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Generating chaos in a test GitLab instance +<!-- vale gitlab.Spelling = NO --> + As [Werner Vogels](https://twitter.com/Werner), the CTO at Amazon Web Services, famously put it, **Everything fails, all the time**. +<!-- vale gitlab.Spelling = NO --> + As a developer, it's as important to consider the failure modes in which your software may operate as much as normal operation. Doing so can mean the difference between a minor hiccup leading to a scattering of `500` errors experienced by a tiny fraction of users, and a full site outage that affects all users for an extended period. To paraphrase [Tolstoy](https://en.wikipedia.org/wiki/Anna_Karenina_principle), _all happy servers are alike, but all failing servers are failing in their own way_. Luckily, there are ways we can attempt to simulate these failure modes, and the chaos endpoints are tools for assisting in this process. diff --git a/doc/development/code_intelligence/index.md b/doc/development/code_intelligence/index.md index c5673f6eee2..02f701645f4 100644 --- a/doc/development/code_intelligence/index.md +++ b/doc/development/code_intelligence/index.md @@ -45,7 +45,7 @@ sequenceDiagram GitLab Rails to authorize the upload. 1. GitLab Rails validates whether the artifact can be uploaded and sends - `ProcessLsif: true` header if the lsif artifact can be processed. + `ProcessLsif: true` header if the LSIF artifact can be processed. 1. Workhorse reads the LSIF document line by line and generates code intelligence data for each file in the project. The output is a zipped directory of JSON diff --git a/doc/development/code_review.md b/doc/development/code_review.md index b9a6f66754b..d9d52401871 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -24,7 +24,7 @@ uncovered edge cases. The default approach is to choose a reviewer from your group or team for the first review. This is only a recommendation and the reviewer may be from a different team. However, it is recommended to pick someone who is a [domain expert](#domain-experts). -If your merge request touches more than one domain (for example, Dynamic Analysis and GraphQL), ask for reviews from an expert from each domain. +If your merge request touches more than one domain (for example, Dynamic Analysis and GraphQL), ask for reviews from an expert from each domain. You can read more about the importance of involving reviewer(s) in the section on the responsibility of the author below. @@ -344,7 +344,7 @@ experience, refactors the existing code). Then: convey your intent. - For non-mandatory suggestions, decorate with (non-blocking) so the author knows they can optionally resolve within the merge request or follow-up at a later stage. - - There's a [Chrome/Firefox addon](https://gitlab.com/conventionalcomments/conventional-comments-button) which you can use to apply [Conventional Comment](https://conventionalcomments.org/) prefixes. + - There's a [Chrome/Firefox add-on](https://gitlab.com/conventionalcomments/conventional-comments-button) which you can use to apply [Conventional Comment](https://conventionalcomments.org/) prefixes. - After a round of line notes, it can be helpful to post a summary note such as "Looks good to me", or "Just a couple things to address." - Assign the merge request to the author if changes are required following your @@ -569,7 +569,7 @@ A good example of collaboration on an MR touching multiple parts of the codebase ### Credits -Largely based on the [thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review). +Largely based on the [`thoughtbot` code review guide](https://github.com/thoughtbot/guides/tree/master/code-review). --- diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index 3ed23dcc298..2a2cfebe964 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -56,12 +56,12 @@ The current Lefthook configuration can be found in [`lefthook.yml`](https://gitl Before you push your changes, Lefthook automatically runs the following checks: - Danger: Runs a subset of checks that `danger-review` runs on your merge requests. -- ES lint: Run `yarn eslint` checks (with the [`.eslintrc.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.eslintrc.yml) config) on the modified `*.{js,vue}` files. Tags: `frontend`, `style`. -- HAML lint: Run `bundle exec haml-lint` checks (with the [`.haml-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.haml-lint.yml) config) on the modified `*.html.haml` files. Tags: `view`, `haml`, `style`. +- ES lint: Run `yarn eslint` checks (with the [`.eslintrc.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.eslintrc.yml) configuration) on the modified `*.{js,vue}` files. Tags: `frontend`, `style`. +- HAML lint: Run `bundle exec haml-lint` checks (with the [`.haml-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.haml-lint.yml) configuration) on the modified `*.html.haml` files. Tags: `view`, `haml`, `style`. - Markdown lint: Run `yarn markdownlint` checks on the modified `*.md` files. Tags: `documentation`, `style`. -- SCSS lint: Run `bundle exec scss-lint` checks (with the [`.scss-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.scss-lint.yml) config) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`. -- RuboCop: Run `bundle exec rubocop` checks (with the [`.rubocop.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.rubocop.yml) config) on the modified `*.rb` files. Tags: `backend`, `style`. -- Vale: Run `vale` checks (with the [`.vale.ini`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.vale.ini) config) on the modified `*.md` files. Tags: `documentation`, `style`. +- SCSS lint: Run `bundle exec scss-lint` checks (with the [`.scss-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.scss-lint.yml) configuration) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`. +- RuboCop: Run `bundle exec rubocop` checks (with the [`.rubocop.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.rubocop.yml) configuration) on the modified `*.rb` files. Tags: `backend`, `style`. +- Vale: Run `vale` checks (with the [`.vale.ini`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.vale.ini) configuration) on the modified `*.md` files. Tags: `documentation`, `style`. In addition to the default configuration, you can define a [local configuration](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#local-config). @@ -142,7 +142,7 @@ This ensures that our list isn't mistakenly removed by another auto generation o the `.rubocop_todo.yml`. This also allows us greater visibility into the exceptions which are currently being resolved. -One way to generate the initial list is to run the todo auto generation, +One way to generate the initial list is to run the `todo` auto generation, with `exclude limit` set to a high number. ```shell @@ -180,8 +180,12 @@ See the dedicated [Shell scripting standards and style guidelines](../shell_scri ## Markdown +<!-- vale gitlab.Spelling = NO --> + We're following [Ciro Santilli's Markdown Style Guide](https://cirosantilli.com/markdown-style-guide/). +<!-- vale gitlab.Spelling = YES --> + ## Documentation See the dedicated [Documentation Style Guide](../documentation/styleguide/index.md). diff --git a/doc/development/database/database_reviewer_guidelines.md b/doc/development/database/database_reviewer_guidelines.md index 26083183d6d..a242a3d5fd0 100644 --- a/doc/development/database/database_reviewer_guidelines.md +++ b/doc/development/database/database_reviewer_guidelines.md @@ -50,17 +50,17 @@ European/US and APAC friendly hours. You can join the office hours call and brin that require a more in-depth discussion between the database reviewers and maintainers: - [Database Office Hours Agenda](https://docs.google.com/document/d/1wgfmVL30F8SdMg-9yY6Y8djPSxWNvKmhR5XmsvYX1EI/edit). -- [Youtube playlist with past recordings](https://www.youtube.com/playlist?list=PL05JrBw4t0Kp-kqXeiF7fF7cFYaKtdqXM). +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [YouTube playlist with past recordings](https://www.youtube.com/playlist?list=PL05JrBw4t0Kp-kqXeiF7fF7cFYaKtdqXM). You should also join the [#database-labs](../understanding_explain_plans.md#database-lab) -Slack channel and get familiar with how to use Joe, the slackbot that provides developers +Slack channel and get familiar with how to use Joe, the Slackbot that provides developers with their own clone of the production database. Understanding and efficiently using `EXPLAIN` plans is at the core of the database review process. The following guides provide a quick introduction and links to follow on more advanced topics: - Guide on [understanding EXPLAIN plans](../understanding_explain_plans.md). -- [Explaining the unexplainable series in depesz](https://www.depesz.com/tag/unexplainable/). +- [Explaining the unexplainable series in `depesz`](https://www.depesz.com/tag/unexplainable/). Finally, you can find various guides in the [Database guides](index.md) page that cover more specific topics and use cases. The most frequently required during database reviewing are the following: diff --git a/doc/development/database/table_partitioning.md b/doc/development/database/table_partitioning.md index 358b9bb42b0..207d5a733ce 100644 --- a/doc/development/database/table_partitioning.md +++ b/doc/development/database/table_partitioning.md @@ -60,7 +60,7 @@ was the first table to be partitioned in the application database (scheduled for deployment with the GitLab 13.5 release). This table tracks audit entries of security events that happen in the application. In almost all cases, users want to see audit activity that -occurs in a certain timeframe. As a result, date-range partitioning +occurs in a certain time frame. As a result, date-range partitioning was a natural fit for how the data would be accessed. To look at this in more detail, imagine a simplified `audit_events` schema: diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index f4558189000..dc64b59018b 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -69,16 +69,14 @@ bundle exec rails db -e development Use these instructions for exploring the GitLab database while developing with the GDK: 1. Install or open [Visual Studio Code](https://code.visualstudio.com/download). -1. Install the [PostgreSQL VSCode Extension](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres) by Chris Kolkman. +1. Install the [PostgreSQL VSCode Extension](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres). 1. In Visual Studio Code click on the PostgreSQL Explorer button in the left toolbar. 1. In the top bar of the new window, click on the `+` to **Add Database Connection**, and follow the prompts to fill in the details: 1. **Hostname**: the path to the PostgreSQL folder in your GDK directory (for example `/dev/gitlab-development-kit/postgresql`). 1. **PostgreSQL user to authenticate as**: usually your local username, unless otherwise specified during PostgreSQL installation. 1. **Password of the PostgreSQL user**: the password you set when installing PostgreSQL. 1. **Port number to connect to**: `5432` (default). - 1. <!-- vale gitlab.Spelling = NO --> - **Use an ssl connection?** - <!-- vale gitlab.Spelling = YES --> This depends on your installation. Options are: + 1. **Use an SSL connection?** This depends on your installation. Options are: - **Use Secure Connection** - **Standard Connection** (default) 1. **(Optional) The database to connect to**: `gitlabhq_development`. diff --git a/doc/development/database_review.md b/doc/development/database_review.md index da2c93cc1fd..cc4acba100b 100644 --- a/doc/development/database_review.md +++ b/doc/development/database_review.md @@ -147,7 +147,7 @@ test its execution using `CREATE INDEX CONCURRENTLY` in the `#database-lab` Slac - Provide the link to the plan at: [explain.depesz.com](https://explain.depesz.com). Paste both the plan and the query used in the form. - When providing query plans, make sure it hits enough data: - You can use a GitLab production replica to test your queries on a large scale, - through the `#database-lab` Slack channel or through [chatops](understanding_explain_plans.md#chatops). + through the `#database-lab` Slack channel or through [ChatOps](understanding_explain_plans.md#chatops). - Usually, the `gitlab-org` namespace (`namespace_id = 9970`) and the `gitlab-org/gitlab-foss` (`project_id = 13083`) or the `gitlab-org/gitlab` (`project_id = 278964`) projects provide enough data to serve as a good example. @@ -220,13 +220,13 @@ test its execution using `CREATE INDEX CONCURRENTLY` in the `#database-lab` Slac - Check for any obviously complex queries and queries the author specifically points out for review (if any) - If not present yet, ask the author to provide SQL queries and query plans - (for example, by using [chatops](understanding_explain_plans.md#chatops) or direct + (for example, by using [ChatOps](understanding_explain_plans.md#chatops) or direct database access) - For given queries, review parameters regarding data distribution - [Check query plans](understanding_explain_plans.md) and suggest improvements to queries (changing the query, schema or adding indexes and similar) - General guideline is for queries to come in below [100ms execution time](query_performance.md#timing-guidelines-for-queries) - - Avoid N+1 problems and minimalize the [query count](merge_request_performance_guidelines.md#query-counts). + - Avoid N+1 problems and minimize the [query count](merge_request_performance_guidelines.md#query-counts). ### Timing guidelines for migrations diff --git a/doc/development/diffs.md b/doc/development/diffs.md index 8f71ad2b7f0..52ba89a4d6e 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -14,12 +14,20 @@ We rely on different sources to present diffs. These include: ## Deep Dive +<!-- vale gitlab.Spelling = NO --> + In January 2019, Oswaldo Ferreira hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab Diffs and Commenting on Diffs -functionality to share his domain specific knowledge with anyone who may work in this part of the -codebase in the future. You can find the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [recording on YouTube](https://www.youtube.com/watch?v=K6G3gMcFyek), -and the slides on [Google Slides](https://docs.google.com/presentation/d/1bGutFH2AT3bxOPZuLMGl1ANWHqFnrxwQwjiwAZkF-TU/edit) -and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/b5ad2f336e0afcfe0f99db0af0ccc71a/). +functionality to share domain-specific knowledge with anyone who may work in this part of the +codebase in the future: + +<!-- vale gitlab.Spelling = YES --> + +- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> + [Recording on YouTube](https://www.youtube.com/watch?v=K6G3gMcFyek) +- Slides on [Google Slides](https://docs.google.com/presentation/d/1bGutFH2AT3bxOPZuLMGl1ANWHqFnrxwQwjiwAZkF-TU/edit) +- [PDF slides](https://gitlab.com/gitlab-org/create-stage/uploads/b5ad2f336e0afcfe0f99db0af0ccc71a/) + Everything covered in this deep dive was accurate as of GitLab 11.7, and while specific details may have changed since then, it should still serve as a good introduction. diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 55f5d43b175..62f74b9b39d 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -514,7 +514,7 @@ To run the tool on an existing screenshot generator, take the following steps: 1. Set up the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/gitlab_docs.md). 1. Navigate to the subdirectory with your cloned GitLab repository, typically `gdk/gitlab`. 1. Make sure that your GDK database is fully migrated: `bin/rake db:migrate RAILS_ENV=development`. -1. Install pngquant, see the tool website for more information: [`pngquant`](https://pngquant.org/) +1. Install `pngquant`, see the tool website for more information: [`pngquant`](https://pngquant.org/) 1. Run `scripts/docs_screenshots.rb spec/docs_screenshots/<name_of_screenshot_generator>.rb <milestone-version>`. 1. Identify the location of the screenshots, based on the `gitlab/doc` location defined by the `it` parameter in your script. 1. Commit the newly created screenshots. diff --git a/doc/development/documentation/restful_api_styleguide.md b/doc/development/documentation/restful_api_styleguide.md index c8c48e71b48..e05f6760ff1 100644 --- a/doc/development/documentation/restful_api_styleguide.md +++ b/doc/development/documentation/restful_api_styleguide.md @@ -165,7 +165,7 @@ curl --request POST \ ### Post data using form-data -Instead of using JSON or urlencode you can use multipart/form-data which +Instead of using JSON or URL-encoding data, you can use `multipart/form-data` which properly handles data encoding: ```shell diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md index be25a083948..92fd17f9d3e 100644 --- a/doc/development/documentation/site_architecture/index.md +++ b/doc/development/documentation/site_architecture/index.md @@ -114,7 +114,7 @@ located in the [Dockerfiles directory](https://gitlab.com/gitlab-org/gitlab-docs If you need to rebuild the Docker images immediately (must have maintainer level permissions): WARNING: -If you change the dockerfile configuration and rebuild the images, you can break the master +If you change the Dockerfile configuration and rebuild the images, you can break the master pipeline in the main `gitlab` repository as well as in `gitlab-docs`. Create an image with a different name first and test it to ensure you do not break the pipelines. diff --git a/doc/development/documentation/testing.md b/doc/development/documentation/testing.md index 7526464a46e..95fcebab81d 100644 --- a/doc/development/documentation/testing.md +++ b/doc/development/documentation/testing.md @@ -246,7 +246,7 @@ when building the `image:docs-lint-markdown` Docker image containing these tools | Tool | Version | Command | Additional information | |--------------------|----------|-------------------------------------------|------------------------| | `markdownlint-cli` | Latest | `yarn global add markdownlint-cli` | n/a | -| `markdownlint-cli` | Specfic | `yarn global add markdownlint-cli@0.23.2` | The `@` indicates a specific version, and this example updates the tool to version `0.23.2`. | +| `markdownlint-cli` | Specific | `yarn global add markdownlint-cli@0.23.2` | The `@` indicates a specific version, and this example updates the tool to version `0.23.2`. | | Vale | Latest | `brew update && brew upgrade vale` | This command is for macOS only. | | Vale | Specific | n/a | Not possible using `brew`, but can be [directly downloaded](https://github.com/errata-ai/vale/releases). | diff --git a/doc/development/integrations/codesandbox.md b/doc/development/integrations/codesandbox.md index faa1ec0ee3f..44a3778dc3e 100644 --- a/doc/development/integrations/codesandbox.md +++ b/doc/development/integrations/codesandbox.md @@ -1,3 +1,9 @@ +--- +stage: none +group: Development +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 +--- + # Set up local Codesandbox development environment This guide walks through setting up a local [Codesandbox repository](https://github.com/codesandbox/codesandbox-client) and integrating it with a local GitLab instance. Codesandbox diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 6c605a44b93..6e27d535b63 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -153,7 +153,7 @@ cd $indexer_path && sudo make install The `gitlab-elasticsearch-indexer` will be installed to `/usr/local/bin`. -You can change the installation path with the `PREFIX` env variable. +You can change the installation path with the `PREFIX` environment variable. Please remember to pass the `-E` flag to `sudo` if you do so. Example: @@ -218,7 +218,7 @@ The following Elasticsearch settings are available: | Parameter | Description | |-------------------------------------------------------|-------------| | `Elasticsearch indexing` | Enables or disables Elasticsearch indexing and creates an empty index if one does not already exist. You may want to enable indexing but disable search in order to give the index time to be fully completed, for example. Also, keep in mind that this option doesn't have any impact on existing data, this only enables/disables the background indexer which tracks data changes and ensures new data is indexed. | -| `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until unpaused. | +| `Pause Elasticsearch indexing` | Enables or disables temporary indexing pause. This is useful for cluster migration/reindexing. All changes are still tracked, but they are not committed to the Elasticsearch index until resumed. | | `Search with Elasticsearch enabled` | Enables or disables using Elasticsearch in search. | | `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., `http://host1, https://host2:9200`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). | | `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, larger indexes need to have more shards. Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). | @@ -260,7 +260,7 @@ from the Elasticsearch index as expected. ## Enabling custom language analyzers -You can improve the language support for Chinese and Japanese languages by utilizing [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) and/or [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) analysis plugins from Elastic. +You can improve the language support for Chinese and Japanese languages by utilizing [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) and/or [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) analysis plugins from Elastic. To enable language(s) support: @@ -276,10 +276,10 @@ For guidance on what to install, see the following Elasticsearch language plugin | Parameter | Description | |-------------------------------------------------------|-------------| -| `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.| -| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [smartcn](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.| -| `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.| -| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [kuromoji](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.| +| `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.| +| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.| +| `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.| +| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.| ## Disabling Advanced Search @@ -317,9 +317,9 @@ used by the GitLab Advanced Search integration. In the **Admin Area > Settings > Advanced Search** section, select the **Pause Elasticsearch Indexing** setting, and then save your change. With this, all updates that should happen on your Elasticsearch index will be -buffered and caught up once unpaused. +buffered and caught up after resuming. -The indexing will also be automatically paused when the [**Trigger cluster reindexing**](#trigger-the-reindex-via-the-advanced-search-administration) button is used, and unpaused when the reindexing completes or aborts. +The indexing will also be automatically paused when the [**Trigger cluster reindexing**](#trigger-the-reindex-via-the-advanced-search-administration) button is used, and resumes when the reindexing completes or aborts. ### Setup @@ -430,7 +430,7 @@ To trigger the re-index from `primary` index: The reindexing is now completed. Your GitLab instance is now ready to use the [automated in-cluster reindexing](#trigger-the-reindex-via-the-advanced-search-administration) feature for future reindexing. -1. Unpause the indexing +1. Resume the indexing Under **Admin Area > Settings > Advanced Search**, uncheck the **Pause Elasticsearch Indexing** setting and save. @@ -448,9 +448,9 @@ After the reindexing is completed, the original index will be scheduled to be de While the reindexing is running, you will be able to follow its progress under that same section. -### Mark the most recent reindex job as failed and unpause the indexing +### Mark the most recent reindex job as failed and resume the indexing -Sometimes, you might want to abandon the unfinished reindex job and unpause the indexing. You can achieve this via the following steps: +Sometimes, you might want to abandon the unfinished reindex job and resume the indexing. You can achieve this via the following steps: 1. Mark the most recent reindex job as failed: @@ -518,8 +518,8 @@ In order to debug issues with the migrations you can check the [`elasticsearch.l Some migrations are built with a retry limit. If the migration cannot finish within the retry limit, it will be halted and a notification will be displayed in the Advanced Search integration settings. It is recommended to check the [`elasticsearch.log` file](../administration/logs.md#elasticsearchlog) to -debug why the migration was halted and make any changes before retrying the migration. Once you believe you've -fixed the cause of the failure, click "Retry migration", and the migration will be scheduled to be retried +debug why the migration was halted and make any changes before retrying the migration. Once you believe you've +fixed the cause of the failure, click "Retry migration", and the migration will be scheduled to be retried in the background. ## GitLab Advanced Search Rake tasks @@ -598,7 +598,7 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic - Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below. - A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This will generally help the cluster stay in good health. - Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB. Another consideration is the number of documents, you should aim for this simple formula for the number of shards: `number of expected documents / 5M +1`. -- `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in realtime. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value. +- `refresh_interval` is a per index setting. You may want to adjust that from default `1s` to a bigger value if you don't need data in real-time. This will change how soon you will see fresh results. If that's important for you, you should leave it as close as possible to the default value. - You might want to raise [`indices.memory.index_buffer_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indexing-buffer.html) to 30% or 40% if you have a lot of heavy indexing operations. ### Advanced Search integration settings guidance @@ -993,7 +993,7 @@ Sometimes there may be issues with your Elasticsearch index data and as such GitLab will allow you to revert to "basic search" when there are no search results and assuming that basic search is supported in that scope. This "basic search" will behave as though you don't have Advanced Search enabled at all for -your instance and search using other data sources (ie. PostgreSQL data and Git +your instance and search using other data sources (such as PostgreSQL data and Git data). ### Data recovery: Elasticsearch is a secondary data store only diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md index db9cd63f464..a0ac4fe6226 100644 --- a/doc/operations/metrics/dashboards/index.md +++ b/doc/operations/metrics/dashboards/index.md @@ -94,7 +94,7 @@ with the **Add Panel** page: ## Duplicate a GitLab-defined dashboard > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/37238) in GitLab 12.7. -> - From [GitLab 12.8 onwards](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard. +> - [GitLab versions 12.8 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/39505), custom metrics are also duplicated when you duplicate a dashboard. You can save a complete copy of a GitLab-defined dashboard along with all custom metrics added to it. The resulting `.yml` file can be customized and adapted to your project. @@ -128,7 +128,7 @@ any chart on a dashboard: The options are: - **Expand panel** - Displays a larger version of a visualization. To return to - the dashboard, click the **Back** button in your browser, or press the <kbd>Esc</kbd> key. + the dashboard, click the **Back** button in your browser, or press the <kbd>Escape</kbd> key. ([Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3100) in GitLab 13.0.) - **View logs** **(ULTIMATE)** - Displays [Logs](../../../user/project/clusters/kubernetes_pod_logs.md), if they are enabled. If used in conjunction with the [timeline zoom](#timeline-zoom-and-url-sharing) @@ -147,7 +147,7 @@ The options are: You can use the **Timeline zoom** function at the bottom of a chart to zoom in on a date and time of your choice. When you click and drag the sliders to select a different beginning or end date of data to display, GitLab adds your selected start -and end times to the URL, enabling you to share specific timeframes more easily. +and end times to the URL, enabling you to share specific time frames more easily. ## Dashboard Annotations diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md index 2a26b71071b..45da283f33e 100644 --- a/doc/security/unlock_user.md +++ b/doc/security/unlock_user.md @@ -40,7 +40,7 @@ To unlock a locked user: user.unlock_access! ``` -1. Exit the console with <kbd>Ctrl</kbd>+<kbd>d</kbd> +1. Exit the console with <kbd>Control</kbd>+<kbd>d</kbd> The user should now be able to log in. diff --git a/doc/university/README.md b/doc/university/README.md index 7d6ecb536a6..26616312f98 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -28,9 +28,13 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres ### 1.1. Version Control and Git +<!-- vale gitlab.Spelling = NO --> + 1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29) 1. [Katacoda: Learn Git Version Control using Interactive Browser-Based Scenarios](https://www.katacoda.com/courses/git) +<!-- vale gitlab.Spelling = YES --> + ### 1.2. GitLab Basics 1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4) @@ -55,11 +59,14 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres ### 1.5. Migrating from other Source Control +<!-- vale gitlab.Spelling = NO --> + 1. [Migrating from Bitbucket/Stash](../user/project/import/bitbucket.md) 1. [Migrating from GitHub](../user/project/import/github.md) 1. [Migrating from SVN](../user/project/import/svn.md) 1. [Migrating from Fogbugz](../user/project/import/fogbugz.md) +<!-- vale gitlab.Spelling = YES --> ### 1.6. The GitLab team 1. [About GitLab](https://about.gitlab.com/company/) @@ -185,6 +192,8 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres ### 3.9. Integrations +<!-- vale gitlab.Spelling = NO --> + 1. [How to Integrate Jira and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415) 1. [How to Integrate Jira with GitLab](../user/project/integrations/jira.md) 1. [How to Integrate Jenkins with GitLab](../integration/jenkins.md) @@ -193,9 +202,11 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres 1. [How to Integrate Convox with GitLab](https://about.gitlab.com/blog/2016/06/09/continuous-delivery-with-gitlab-and-convox/) 1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/blog/2016/05/05/getting-started-gitlab-and-shippable/) +<!-- vale gitlab.Spelling = YES --> + ## 4. External Articles -1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460) +1. [2011 Wall Street Journal article - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](https://cdixon.org/2014/04/13/software-eats-software-development/) 1. [2015 Venture Beat article - Actually, Open Source is Eating the World](https://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/) diff --git a/lib/api/api.rb b/lib/api/api.rb index 9ce580d0b98..d660d9b7536 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -268,6 +268,7 @@ module API mount ::API::Release::Links mount ::API::RemoteMirrors mount ::API::Repositories + mount ::API::ResourceAccessTokens mount ::API::Search mount ::API::Services mount ::API::Settings diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb new file mode 100644 index 00000000000..66948f9eaf3 --- /dev/null +++ b/lib/api/resource_access_tokens.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module API + class ResourceAccessTokens < ::API::Base + include PaginationParams + + before { authenticate! } + + feature_category :authentication_and_authorization + + %w[project].each do |source_type| + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get list of all access tokens for the specified resource' do + detail 'This feature was introduced in GitLab 13.9.' + end + params do + requires :id, type: String, desc: "The #{source_type} ID" + end + get ":id/access_tokens" do + resource = find_source(source_type, params[:id]) + + next unauthorized! unless has_permission_to_read?(resource) + + tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute + + present paginate(tokens), with: Entities::PersonalAccessToken + end + + desc 'Revoke a resource access token' do + detail 'This feature was introduced in GitLab 13.9.' + end + params do + requires :id, type: String, desc: "The #{source_type} ID" + requires :token_id, type: String, desc: "The ID of the token" + end + delete ':id/access_tokens/:token_id' do + resource = find_source(source_type, params[:id]) + token = find_token(resource, params[:token_id]) + + if token.nil? + next not_found!("Could not find #{source_type} access token with token_id: #{params[:token_id]}") + end + + service = ::ResourceAccessTokens::RevokeService.new( + current_user, + resource, + token + ).execute + + service.success? ? no_content! : bad_request!(service.message) + end + + desc 'Create a resource access token' do + detail 'This feature was introduced in GitLab 13.9.' + end + params do + requires :id, type: String, desc: "The #{source_type} ID" + requires :name, type: String, desc: "Resource access token name" + requires :scopes, type: Array[String], desc: "The permissions of the token" + optional :expires_at, type: Date, desc: "The expiration date of the token" + end + post ':id/access_tokens' do + resource = find_source(source_type, params[:id]) + + token_response = ::ResourceAccessTokens::CreateService.new( + current_user, + resource, + declared_params + ).execute + + if token_response.success? + present token_response.payload[:access_token], with: Entities::PersonalAccessToken + else + bad_request!(token_response.message) + end + end + end + end + + helpers do + def find_source(source_type, id) + public_send("find_#{source_type}!", id) # rubocop:disable GitlabSecurity/PublicSend + end + + def find_token(resource, token_id) + PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).find_by_id(token_id) + end + + def has_permission_to_read?(resource) + can?(current_user, :project_bot_access, resource) || can?(current_user, :admin_resource_access_tokens, resource) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 83ff2b261c6..09a3a16907f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3062,6 +3062,9 @@ msgstr "" msgid "An error occured while saving changes: %{error}" msgstr "" +msgid "An error occured while updating the notification settings. Please try again." +msgstr "" + msgid "An error occurred adding a draft to the thread." msgstr "" diff --git a/package.json b/package.json index 4b37c7ed640..e5c440399ba 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "codesandbox-api": "0.0.23", "compression-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^5.0.5", - "core-js": "^3.6.4", + "core-js": "^3.8.3", "cron-validator": "^1.1.1", "cropper": "^2.3.0", "css-loader": "^2.1.1", diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index f7f0592a315..b370b779afe 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -37,7 +37,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows only the SSH clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy SSH clone URL') expect(page).not_to have_content('Copy HTTP clone URL') @@ -66,7 +69,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows only the HTTP clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy HTTP clone URL') expect(page).not_to have_content('Copy SSH clone URL') @@ -97,7 +103,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do it 'shows both SSH and HTTP clone information' do resize_screen_xs visit_project - find('.dropdown-toggle').click + + within('.js-mobile-git-clone') do + find('.dropdown-toggle').click + end expect(page).to have_content('Copy HTTP clone URL') expect(page).to have_content('Copy SSH clone URL') diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb index d444ea27d35..5f7d9b0963b 100644 --- a/spec/features/projects/show/user_manages_notifications_spec.rb +++ b/spec/features/projects/show/user_manages_notifications_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do let(:project) { create(:project, :public, :repository) } before do + stub_feature_flags(vue_notification_dropdown: false) sign_in(project.owner) end diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js new file mode 100644 index 00000000000..38040aab0ca --- /dev/null +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -0,0 +1,221 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; +import { GlButtonGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import httpStatus from '~/lib/utils/http_status'; +import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue'; +import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue'; + +const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled']; +const mockToastShow = jest.fn(); + +describe('NotificationsDropdown', () => { + let wrapper; + let mockAxios; + + function createComponent(injectedProperties = {}) { + return shallowMount(NotificationsDropdown, { + stubs: { + GlButtonGroup, + GlDropdown, + GlDropdownItem, + NotificationsDropdownItem, + }, + directives: { + GlTooltip: createMockDirective(), + }, + provide: { + ...injectedProperties, + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + }); + } + + const findButtonGroup = () => wrapper.find(GlButtonGroup); + const findDropdown = () => wrapper.find(GlDropdown); + const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); + const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem); + const findDropdownItemAt = (index) => + findAllNotificationsDropdownItems().at(index).find(GlDropdownItem); + + const clickDropdownItemAt = async (index) => { + const dropdownItem = findDropdownItemAt(index); + dropdownItem.vm.$emit('click'); + + await waitForPromises(); + }; + + beforeEach(() => { + gon.api_version = 'v4'; + mockAxios = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + mockAxios.restore(); + }); + + describe('template', () => { + describe('when notification level is "custom"', () => { + beforeEach(() => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'custom', + }); + }); + + it('renders a button group', () => { + expect(findButtonGroup().exists()).toBe(true); + }); + }); + + describe('when notification level is not "custom"', () => { + beforeEach(() => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + }); + }); + + it('does not render a button group', () => { + expect(findButtonGroup().exists()).toBe(false); + }); + }); + + describe('button tooltip', () => { + const tooltipTitlePrefix = 'Notification setting'; + it.each` + level | title + ${'global'} | ${'Global'} + ${'watch'} | ${'Watch'} + ${'participating'} | ${'Participate'} + ${'mention'} | ${'On mention'} + ${'disabled'} | ${'Disabled'} + ${'custom'} | ${'Custom'} + `(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: level, + }); + + const tooltipElement = findByTestId('notificationButton'); + const tooltip = getBinding(tooltipElement.element, 'gl-tooltip'); + + expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`); + }); + }); + + describe('button icon', () => { + beforeEach(() => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'disabled', + }); + }); + + it('renders the "notifications-off" icon when notification level is "disabled"', () => { + expect(findDropdown().props('icon')).toBe('notifications-off'); + }); + + it('renders the "notifications" icon when notification level is not "disabled"', () => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + }); + + expect(findDropdown().props('icon')).toBe('notifications'); + }); + }); + + describe('dropdown items', () => { + it.each` + dropdownIndex | level | title | description + ${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'} + ${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'} + ${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'} + ${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'} + ${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'} + ${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'} + `('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + }); + + expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title); + expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe( + description, + ); + }); + }); + }); + + describe('when selecting an item', () => { + beforeEach(() => { + jest.spyOn(axios, 'put'); + }); + + it.each` + projectId | groupId | endpointUrl | endpointType | condition + ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'} + ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'} + ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'} + `( + 'calls the $endpointType endpoint when $condition', + async ({ projectId, groupId, endpointUrl }) => { + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + projectId, + groupId, + }); + + await clickDropdownItemAt(1); + + expect(axios.put).toHaveBeenCalledWith(endpointUrl, { + level: 'watch', + }); + }, + ); + + it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {}); + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + }); + + const dropdownItem = findDropdownItemAt(1); + + await clickDropdownItemAt(1); + + expect(wrapper.vm.selectedNotificationLevel).toBe('watch'); + expect(dropdownItem.props('isChecked')).toBe(true); + }); + + it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); + wrapper = createComponent({ + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + }); + + await clickDropdownItemAt(1); + + expect(wrapper.vm.selectedNotificationLevel).toBe('global'); + expect( + mockToastShow, + ).toHaveBeenCalledWith( + 'An error occured while updating the notification settings. Please try again.', + { type: 'error' }, + ); + }); + }); +}); diff --git a/spec/frontend/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js index c09d9232569..dae24efd041 100644 --- a/spec/frontend/pipelines/blank_state_spec.js +++ b/spec/frontend/pipelines/blank_state_spec.js @@ -1,25 +1,20 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import component from '~/pipelines/components/pipelines_list/blank_state.vue'; +import { mount } from '@vue/test-utils'; +import { getByText } from '@testing-library/dom'; +import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue'; describe('Pipelines Blank State', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(component); - - vm = mountComponent(Component, { + const wrapper = mount(BlankState, { + propsData: { svgPath: 'foo', message: 'Blank State', - }); + }, }); it('should render svg', () => { - expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo'); + expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo'); }); it('should render message', () => { - expect(vm.$el.querySelector('h4').textContent.trim()).toEqual('Blank State'); + expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy(); }); }); diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index e6650549f7f..6a4a7ecd13e 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -468,6 +468,49 @@ RSpec.describe ProjectPolicy do end end + context "project bots" do + let(:project_bot) { create(:user, :project_bot) } + let(:user) { create(:user) } + + context "project_bot_access" do + context "when regular user and part of the project" do + let(:current_user) { user } + + before do + project.add_developer(user) + end + + it { is_expected.not_to be_allowed(:project_bot_access)} + end + + context "when project bot and not part of the project" do + let(:current_user) { project_bot } + + it { is_expected.not_to be_allowed(:project_bot_access)} + end + + context "when project bot and part of the project" do + let(:current_user) { project_bot } + + before do + project.add_developer(project_bot) + end + + it { is_expected.to be_allowed(:project_bot_access)} + end + end + + context 'with resource access tokens' do + let(:current_user) { project_bot } + + before do + project.add_maintainer(project_bot) + end + + it { is_expected.not_to be_allowed(:admin_resource_access_tokens)} + end + end + describe 'read_prometheus_alerts' do context 'with admin' do let(:current_user) { admin } diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb new file mode 100644 index 00000000000..9fd7eb2177d --- /dev/null +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe API::ResourceAccessTokens do + context "when the resource is a project" do + let_it_be(:project) { create(:project) } + let_it_be(:other_project) { create(:project) } + let_it_be(:user) { create(:user) } + + describe "GET projects/:id/access_tokens" do + subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) } + + context "when the user has maintainer permissions" do + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) } + let_it_be(:project_id) { project.id } + + before do + project.add_maintainer(user) + project.add_maintainer(project_bot) + end + + it "gets a list of access tokens for the specified project" do + get_tokens + + token_ids = json_response.map { |token| token['id'] } + + expect(response).to have_gitlab_http_status(:ok) + expect(token_ids).to match_array(access_tokens.pluck(:id)) + end + + context "when using a project access token to GET other project access tokens" do + let_it_be(:token) { access_tokens.first } + + it "gets a list of access tokens for the specified project" do + get api("/projects/#{project_id}/access_tokens", personal_access_token: token) + + token_ids = json_response.map { |token| token['id'] } + + expect(response).to have_gitlab_http_status(:ok) + expect(token_ids).to match_array(access_tokens.pluck(:id)) + end + end + + context "when tokens belong to a different project" do + let_it_be(:bot) { create(:user, :project_bot) } + let_it_be(:token) { create(:personal_access_token, user: bot) } + + before do + other_project.add_maintainer(bot) + other_project.add_maintainer(user) + end + + it "does not return tokens from a different project" do + get_tokens + + token_ids = json_response.map { |token| token['id'] } + + expect(token_ids).not_to include(token.id) + end + end + + context "when the project has no access tokens" do + let(:project_id) { other_project.id } + + before do + other_project.add_maintainer(user) + end + + it 'returns an empty array' do + get_tokens + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq([]) + end + end + + context "when trying to get the tokens of a different project" do + let_it_be(:project_id) { other_project.id } + + it "returns 404" do + get_tokens + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when the project does not exist" do + let(:project_id) { non_existing_record_id } + + it "returns 404" do + get_tokens + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context "when the user does not have valid permissions" do + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) } + let_it_be(:project_id) { project.id } + + before do + project.add_developer(user) + project.add_maintainer(project_bot) + end + + it "returns 401" do + get_tokens + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do + subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) } + + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:token) { create(:personal_access_token, user: project_bot) } + let_it_be(:project_id) { project.id } + let_it_be(:token_id) { token.id } + + before do + project.add_maintainer(project_bot) + end + + context "when the user has maintainer permissions" do + before do + project.add_maintainer(user) + end + + it "deletes the project access token from the project" do + delete_token + + expect(response).to have_gitlab_http_status(:no_content) + expect(User.exists?(project_bot.id)).to be_falsy + end + + context "when attempting to delete a non-existent project access token" do + let_it_be(:token_id) { non_existing_record_id } + + it "does not delete the token, and returns 404" do + delete_token + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to include("Could not find project access token with token_id: #{token_id}") + end + end + + context "when attempting to delete a token that does not belong to the specified project" do + let_it_be(:project_id) { other_project.id } + + before do + other_project.add_maintainer(user) + end + + it "does not delete the token, and returns 404" do + delete_token + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to include("Could not find project access token with token_id: #{token_id}") + end + end + end + + context "when the user does not have valid permissions" do + before do + project.add_developer(user) + end + + it "does not delete the token, and returns 400", :aggregate_failures do + delete_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(User.exists?(project_bot.id)).to be_truthy + expect(response.body).to include("#{user.name} cannot delete #{token.user.name}") + end + end + end + + describe "POST projects/:id/access_tokens" do + let_it_be(:params) { { name: "test", scopes: ["api"], expires_at: Date.today + 1.month } } + + subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params } + + context "when the user has maintainer permissions" do + let_it_be(:project_id) { project.id } + let_it_be(:expires_at) { 1.month.from_now } + + before do + project.add_maintainer(user) + end + + context "with valid params" do + context "with full params" do + it "creates a project access token with the params", :aggregate_failures do + create_token + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["name"]).to eq("test") + expect(json_response["scopes"]).to eq(["api"]) + expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601) + end + end + + context "when 'expires_at' is not set" do + let_it_be(:params) { { name: "test", scopes: ["api"] } } + + it "creates a project access token with the params", :aggregate_failures do + create_token + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["name"]).to eq("test") + expect(json_response["scopes"]).to eq(["api"]) + expect(json_response["expires_at"]).to eq(nil) + end + end + end + + context "with invalid params" do + context "when missing the 'name' param" do + let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } } + + it "does not create a project access token without 'name'" do + create_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include("name is missing") + end + end + + context "when missing the 'scopes' param" do + let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } } + + it "does not create a project access token without 'scopes'" do + create_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include("scopes is missing") + end + end + end + + context "when trying to create a token in a different project" do + let_it_be(:project_id) { other_project.id } + + it "does not create the token, and returns the project not found error" do + create_token + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to include("Project Not Found") + end + end + end + + context "when the user does not have valid permissions" do + let_it_be(:project_id) { project.id } + + context "when the user is a developer" do + before do + project.add_developer(user) + end + + it "does not create the token, and returns the permission error" do + create_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include("User does not have permission to create project access token") + end + end + + context "when a project access token tries to create another project access token" do + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:user) { project_bot } + + before do + project.add_maintainer(user) + end + + it "does not allow a project access token to create another project access token" do + create_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include("User does not have permission to create project access token") + end + end + end + end + end +end diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb index a53f1fce515..9999e674c7d 100644 --- a/spec/services/users/approve_service_spec.rb +++ b/spec/services/users/approve_service_spec.rb @@ -61,6 +61,14 @@ RSpec.describe Users::ApproveService do expect(user.reload).to be_active end + it 'logs approval in application logs' do + allow(Gitlab::AppLogger).to receive(:info) + + subject + + expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end + it 'emails the user on approval' do expect(DeviseMailer).to receive(:user_admin_approval).with(user).and_call_original expect { subject }.to have_enqueued_mail(DeviseMailer, :user_admin_approval) diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb index d050376b6ff..b9aaff5cde5 100644 --- a/spec/services/users/reject_service_spec.rb +++ b/spec/services/users/reject_service_spec.rb @@ -48,6 +48,14 @@ RSpec.describe Users::RejectService do subject end + + it 'logs rejection in application logs' do + allow(Gitlab::AppLogger).to receive(:info) + + subject + + expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end end end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 548dba7874a..cc0eb9919da 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'projects/_home_panel' do let(:project) { create(:project) } before do + stub_feature_flags(vue_notification_dropdown: false) assign(:project, project) allow(view).to receive(:current_user).and_return(user) diff --git a/yarn.lock b/yarn.lock index 72fe9d33e45..7ea154d2d5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3334,10 +3334,10 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== -core-js@^3.1.3, core-js@^3.6.4: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" - integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== +core-js@^3.1.3, core-js@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" + integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== core-js@~2.3.0: version "2.3.0" |