diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-17 21:09:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-17 21:09:19 +0300 |
commit | c0ef148ef349f0d13331638ab90f5d9e9d2175ba (patch) | |
tree | fd826213aa30339d8fee4bcc1bf35c6e40639823 | |
parent | c982bb363b3a0390a274197f410a1609a4667760 (diff) |
Add latest changes from gitlab-org/gitlab@master
156 files changed, 1816 insertions, 633 deletions
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e72c5c90986..445602a8765 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,6 +3,7 @@ import Dropzone from 'dropzone'; import $ from 'jquery'; import { sprintf, __ } from '~/locale'; +import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; import { visitUrl } from '../lib/utils/url_utility'; @@ -83,6 +84,9 @@ export default class BlobFileDropzone { submitButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); + + trackUploadFileFormSubmitted(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert alert(__('Please select a file')); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 173c82ef9b0..6d9b56b4bb8 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; +import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; import Tracking from '~/tracking'; import BlobFileDropzone from '../blob/blob_file_dropzone'; import NewCommitForm from '../new_commit_form'; @@ -47,6 +48,7 @@ export const initUploadForm = () => { new NewCommitForm(uploadBlobForm); disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file'); + initUploadFileTrigger(); } }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 920ffde3e32..a71bf0d874d 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -34,7 +34,6 @@ export default () => { helpPagePath: pipelineTableViewEl.dataset.helpPagePath, emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }); }, diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 787152d00ef..926f5b82b66 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -29,10 +29,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, errorStateSvgPath: { type: String, required: true, @@ -212,7 +208,6 @@ export default { <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" > <template #table-header-actions> diff --git a/app/assets/javascripts/experiment_tracking.js b/app/assets/javascripts/experiment_tracking.js new file mode 100644 index 00000000000..4611af3f857 --- /dev/null +++ b/app/assets/javascripts/experiment_tracking.js @@ -0,0 +1,25 @@ +import { get } from 'lodash'; +import Tracking from '~/tracking'; + +const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0'; + +export default class ExperimentTracking { + constructor(experimentName, { label } = {}) { + this.label = label; + this.experimentData = get(window, ['gon', 'global', 'experiment', experimentName]); + } + + event(action) { + if (!this.experimentData) { + return false; + } + + return Tracking.event(document.body.dataset.page, action, { + label: this.label, + context: { + schema: TRACKING_CONTEXT_SCHEMA, + data: this.experimentData, + }, + }); + } +} diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue index af4e9acf4ba..22a767cfaae 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue @@ -1,7 +1,16 @@ <script> -import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui'; +import { + GlFormGroup, + GlFormCheckbox, + GlFormRadio, + GlFormInput, + GlLink, + GlSprintf, +} from '@gitlab/ui'; import { mapGetters } from 'vuex'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import eventHub from '../event_hub'; const commentDetailOptions = [ { @@ -18,12 +27,41 @@ const commentDetailOptions = [ }, ]; +const ISSUE_TRANSITION_AUTO = 'auto'; +const ISSUE_TRANSITION_CUSTOM = 'custom'; + +const issueTransitionOptions = [ + { + value: ISSUE_TRANSITION_AUTO, + label: s__('JiraService|Move to Done'), + help: s__( + 'JiraService|Automatically transitions Jira issues to the "Done" category. %{linkStart}Learn more%{linkEnd}', + ), + link: helpPagePath('user/project/integrations/jira.html', { + anchor: 'automatic-issue-transitions', + }), + }, + { + value: ISSUE_TRANSITION_CUSTOM, + label: s__('JiraService|Use custom transitions'), + help: s__( + 'JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}', + ), + link: helpPagePath('user/project/integrations/jira.html', { + anchor: 'custom-issue-transitions', + }), + }, +]; + export default { name: 'JiraTriggerFields', components: { GlFormGroup, GlFormCheckbox, GlFormRadio, + GlFormInput, + GlLink, + GlSprintf, }, props: { initialTriggerCommit: { @@ -43,21 +81,52 @@ export default { required: false, default: 'standard', }, + initialJiraIssueTransitionId: { + type: String, + required: false, + default: '', + }, }, data() { return { + validated: false, triggerCommit: this.initialTriggerCommit, triggerMergeRequest: this.initialTriggerMergeRequest, enableComments: this.initialEnableComments, commentDetail: this.initialCommentDetail, + jiraIssueTransitionId: this.initialJiraIssueTransitionId, + issueTransitionMode: this.initialJiraIssueTransitionId.length + ? ISSUE_TRANSITION_CUSTOM + : ISSUE_TRANSITION_AUTO, commentDetailOptions, + issueTransitionOptions, }; }, computed: { ...mapGetters(['isInheriting']), - showEnableComments() { + showTriggerSettings() { return this.triggerCommit || this.triggerMergeRequest; }, + validIssueTransitionId() { + return !this.validated || this.jiraIssueTransitionId.length > 0; + }, + }, + created() { + eventHub.$on('validateForm', this.validateForm); + }, + beforeDestroy() { + eventHub.$off('validateForm', this.validateForm); + }, + methods: { + validateForm() { + this.validated = true; + }, + showCustomIssueTransitions(currentOption) { + return ( + this.issueTransitionMode === ISSUE_TRANSITION_CUSTOM && + currentOption === ISSUE_TRANSITION_CUSTOM + ); + }, }, }; </script> @@ -89,7 +158,7 @@ export default { </gl-form-group> <gl-form-group - v-show="showEnableComments" + v-show="showTriggerSettings" :label="s__('Integrations|Comment settings:')" label-for="service[comment_on_event_enabled]" class="gl-pl-6" @@ -106,7 +175,7 @@ export default { </gl-form-group> <gl-form-group - v-show="showEnableComments && enableComments" + v-show="showTriggerSettings && enableComments" :label="s__('Integrations|Comment detail:')" label-for="service[comment_detail]" class="gl-pl-9" @@ -126,5 +195,51 @@ export default { </template> </gl-form-radio> </gl-form-group> + + <gl-form-group + v-show="showTriggerSettings" + :label="s__('JiraService|Transition Jira issues to their final state:')" + class="gl-pl-6" + data-testid="issue-transition-settings" + > + <input type="hidden" name="service[jira_issue_transition_id]" value="" /> + + <gl-form-radio + v-for="issueTransitionOption in issueTransitionOptions" + :key="issueTransitionOption.value" + v-model="issueTransitionMode" + :value="issueTransitionOption.value" + :disabled="isInheriting" + :data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`" + > + {{ issueTransitionOption.label }} + + <template v-if="showCustomIssueTransitions(issueTransitionOption.value)"> + <gl-form-input + v-model="jiraIssueTransitionId" + name="service[jira_issue_transition_id]" + type="text" + class="gl-my-3" + data-qa-selector="service_jira_issue_transition_id_field" + :placeholder="s__('JiraService|For example, 12, 24')" + :disabled="isInheriting" + :required="true" + :state="validIssueTransitionId" + /> + + <span class="invalid-feedback"> + {{ s__('This field is required.') }} + </span> + </template> + + <template #help> + <gl-sprintf :message="issueTransitionOption.help"> + <template #link="{ content }"> + <gl-link :href="issueTransitionOption.link" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + </gl-form-radio> + </gl-form-group> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js index ab9bdd9ca2e..1ae353ab6e3 100644 --- a/app/assets/javascripts/integrations/edit/index.js +++ b/app/assets/javascripts/integrations/edit/index.js @@ -28,6 +28,7 @@ function parseDatasetToProps(data) { testPath, resetPath, vulnerabilitiesIssuetype, + jiraIssueTransitionId, ...booleanAttributes } = data; const { @@ -59,6 +60,7 @@ function parseDatasetToProps(data) { initialTriggerMergeRequest: mergeRequestEvents, initialEnableComments: enableComments, initialCommentDetail: commentDetail, + initialJiraIssueTransitionId: jiraIssueTransitionId, }, jiraIssuesProps: { showJiraIssuesIntegration, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 4dab796d8a4..64c49b4276e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -364,7 +364,6 @@ export default class MergeRequestTabs { helpPagePath: pipelineTableViewEl.dataset.helpPagePath, emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, canCreatePipelineInTargetProject: Boolean( mrWidgetData?.can_create_pipeline_in_target_project, ), diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 0494dad6e33..e5ec9976ac5 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -24,9 +24,12 @@ new UserCallout({ }); // Project show page loads different overview content based on user preferences -const treeSlider = document.getElementById('js-tree-list'); -if (treeSlider) { + +if (document.querySelector('.js-upload-blob-form')) { initUploadForm(); +} + +if (document.getElementById('js-tree-list')) { initTree(); } diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 823ada133d2..a61ffe8f0fc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,5 +1,6 @@ <script> import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { SCHEDULE_ORIGIN } from '../../constants'; export default { @@ -26,10 +27,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, }, computed: { user() { @@ -44,6 +41,12 @@ export default { this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`, ); }, + autoDevopsTagId() { + return `pipeline-url-autodevops-${this.pipeline.id}`; + }, + autoDevopsHelpPath() { + return helpPagePath('topics/autodevops/index.md'); + }, }, }; </script> @@ -103,38 +106,43 @@ export default { data-testid="pipeline-url-failure" >{{ __('error') }}</gl-badge > - <gl-link - v-if="pipeline.flags.auto_devops" - :id="`pipeline-url-autodevops-${pipeline.id}`" - tabindex="0" - data-testid="pipeline-url-autodevops" - role="button" - ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link - > - <gl-popover - :target="`pipeline-url-autodevops-${pipeline.id}`" - triggers="focus" - placement="top" - > - <template #title> - <div class="gl-font-weight-normal gl-line-height-normal"> - <gl-sprintf - :message=" - __( - 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', - ) - " - > - <template #strong="{ content }"> - <b>{{ content }}</b> - </template> - </gl-sprintf> - </div> - </template> - <gl-link :href="autoDevopsHelpPath" target="_blank" rel="noopener noreferrer nofollow">{{ - __('Learn more about Auto DevOps') - }}</gl-link> - </gl-popover> + <template v-if="pipeline.flags.auto_devops"> + <gl-link + :id="autoDevopsTagId" + tabindex="0" + data-testid="pipeline-url-autodevops" + role="button" + > + <gl-badge variant="info" size="sm"> + {{ __('Auto DevOps') }} + </gl-badge> + </gl-link> + <gl-popover :target="autoDevopsTagId" triggers="focus" placement="top"> + <template #title> + <div class="gl-font-weight-normal gl-line-height-normal"> + <gl-sprintf + :message=" + __( + 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', + ) + " + > + <template #strong="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + </div> + </template> + <gl-link + :href="autoDevopsHelpPath" + data-testid="pipeline-url-autodevops-link" + target="_blank" + > + {{ __('Learn more about Auto DevOps') }} + </gl-link> + </gl-popover> + </template> + <gl-badge v-if="pipeline.flags.stuck" variant="warning" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 48009a9fcb8..30a099fd1c7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -68,10 +68,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, hasGitlabCi: { type: Boolean, required: true, @@ -362,7 +358,6 @@ export default { :pipelines="state.pipelines" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 24c67184e56..fdc8c3e1866 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -32,10 +32,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -102,7 +98,6 @@ export default { :pipeline="model" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" :canceling-pipeline="cancelingPipeline" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index 572abe2a24a..68deca313eb 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -47,10 +47,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -194,11 +190,7 @@ export default { </div> </div> - <pipeline-url - :pipeline="pipeline" - :pipeline-schedule-url="pipelineScheduleUrl" - :auto-devops-help-path="autoDevopsHelpPath" - /> + <pipeline-url :pipeline="pipeline" :pipeline-schedule-url="pipelineScheduleUrl" /> <pipeline-triggerer :pipeline="pipeline" /> <div class="table-section section-wrap section-20"> diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js index 7bcc51e18e5..a7dea2cf103 100644 --- a/app/assets/javascripts/pipelines/pipelines_index.js +++ b/app/assets/javascripts/pipelines/pipelines_index.js @@ -27,7 +27,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline, hasGitlabCi, @@ -60,7 +59,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline: parseBoolean(canCreatePipeline), hasGitlabCi: parseBoolean(hasGitlabCi), diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file_experiment.js new file mode 100644 index 00000000000..c2a68043489 --- /dev/null +++ b/app/assets/javascripts/projects/upload_file_experiment.js @@ -0,0 +1,21 @@ +import ExperimentTracking from '~/experiment_tracking'; + +function trackEvent(eventName) { + const Tracking = new ExperimentTracking('empty_repo_upload', { label: 'blob-upload-modal' }); + + Tracking.event(eventName); +} + +export function initUploadFileTrigger() { + const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger'); + + if (uploadFileTriggerEl) { + uploadFileTriggerEl.addEventListener('click', () => { + trackEvent('click_upload_modal_trigger'); + }); + } +} + +export function trackUploadFileFormSubmitted() { + trackEvent('click_upload_modal_form_submit'); +} diff --git a/app/controllers/concerns/comment_and_close_flag.rb b/app/controllers/concerns/comment_and_close_flag.rb index e2f3272abbc..02a68d2857c 100644 --- a/app/controllers/concerns/comment_and_close_flag.rb +++ b/app/controllers/concerns/comment_and_close_flag.rb @@ -5,7 +5,7 @@ module CommentAndCloseFlag included do before_action do - push_frontend_feature_flag(:remove_comment_close_reopen, @group) + push_frontend_feature_flag(:remove_comment_close_reopen, @group || @project&.group) end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ebffb62cff3..d6ba14f4f7f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -329,6 +329,8 @@ class ProjectsController < Projects::ApplicationController if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + experiment(:empty_repo_upload, project: @project).track(:view_project_show) if @project.can_current_user_push_to_default_branch? + if @project.empty_repo? record_experiment_user(:invite_members_empty_project_version_a) diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb index 2854226e178..04b124ddd83 100644 --- a/app/finders/packages/npm/package_finder.rb +++ b/app/finders/packages/npm/package_finder.rb @@ -2,29 +2,41 @@ module Packages module Npm class PackageFinder - attr_reader :project, :package_name - delegate :find_by_version, to: :execute + delegate :last, to: :execute - def initialize(project, package_name) - @project = project + def initialize(package_name, project: nil, namespace: nil) @package_name = package_name + @project = project + @namespace = namespace end def execute - return Packages::Package.none unless project - - packages + base.npm + .with_name(@package_name) + .last_of_each_version + .preload_files end private - def packages - project.packages - .npm - .with_name(package_name) - .last_of_each_version - .preload_files + def base + if @project + packages_for_project + elsif @namespace + packages_for_namespace + else + ::Packages::Package.none + end + end + + def packages_for_project + @project.packages + end + + def packages_for_namespace + projects = ::Project.in_namespace(@namespace.self_and_descendants.select(:id)) + ::Packages::Package.for_projects(projects.select(:id)) end end end diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index ed28c3ffd7e..007d86f60b8 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -67,7 +67,10 @@ module Types end self.define_singleton_method(:suitable?) do |gid| - gid&.model_class&.ancestors&.include?(model_class) + next false if gid.nil? + + gid.model_name.safe_constantize.present? && + gid.model_class.ancestors.include?(model_class) end self.define_singleton_method(:coerce_input) do |string, ctx| diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 889365e39de..961ef613a06 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -23,6 +23,14 @@ module InviteMembersHelper end end + def show_invite_members_track_event + if directly_invite_members? + 'show_invite_members' + elsif indirectly_invite_members? + 'show_invite_members_version_b' + end + end + def invite_group_members?(group) experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group) end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 14d20e7c622..8fc63dcdb3a 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -86,7 +86,7 @@ module ServicesHelper end def integration_form_data(integration, group: nil) - { + form_data = { id: integration.id, show_active: integration.show_active_box?.to_s, activated: (integration.active || integration.new_record?).to_s, @@ -106,6 +106,12 @@ module ServicesHelper test_path: scoped_test_integration_path(integration), reset_path: scoped_reset_integration_path(integration, group: group) } + + if integration.is_a?(JiraService) + form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id + end + + form_data end def trigger_events_for_service(integration) diff --git a/app/helpers/stat_anchors_helper.rb b/app/helpers/stat_anchors_helper.rb index 1e8e6371284..d9429f28be7 100644 --- a/app/helpers/stat_anchors_helper.rb +++ b/app/helpers/stat_anchors_helper.rb @@ -5,13 +5,14 @@ module StatAnchorsHelper {}.tap do |attrs| attrs[:class] = %w(nav-link gl-display-flex gl-align-items-center) << extra_classes(anchor) attrs[:itemprop] = anchor.itemprop if anchor.itemprop + attrs[:data] = anchor.data if anchor.data end end private def button_attribute(anchor) - "btn-#{anchor.class_modifier || 'dashed'}" + anchor.class_modifier || 'btn-dashed' end def extra_classes(anchor) diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 391540634be..0673edfd831 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -40,11 +40,11 @@ class Packages::Package < ApplicationRecord validate :unique_debian_package_name, if: :debian_package? validate :valid_conan_package_recipe, if: :conan? - validate :valid_npm_package_name, if: :npm? validate :valid_composer_global_name, if: :composer? validate :package_already_taken, if: :npm? validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? + validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm? validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget? validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package? validates :name, inclusion: { in: %w[incoming] }, if: :debian_incoming? @@ -247,14 +247,6 @@ class Packages::Package < ApplicationRecord end end - def valid_npm_package_name - return unless project&.root_namespace - - unless name =~ %r{\A@#{project.root_namespace.path}/[^/]+\z} - errors.add(:name, 'is not valid') - end - end - def package_already_taken return unless project diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5857d86f921..12f7bac23e4 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -124,15 +124,11 @@ class JiraService < IssueTrackerService end def fields - transition_id_help_path = help_page_path('user/project/integrations/jira', anchor: 'obtaining-a-transition-id') - transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">'.html_safe % { transition_id_help_path: transition_id_help_path } - [ { type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'api_url', title: s_('JiraService|Jira API URL'), placeholder: s_('JiraService|If different from Web URL') }, { type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: s_('JiraService|Use a username for server version and an email for cloud version'), required: true }, - { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true }, - { type: 'text', name: 'jira_issue_transition_id', title: s_('JiraService|Jira workflow transition IDs'), placeholder: s_('JiraService|For example, 12, 24'), help: s_('JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}'.html_safe % { link_start: transition_id_help_link_start, link_end: '</a>'.html_safe }) } + { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true } ] end @@ -159,17 +155,19 @@ class JiraService < IssueTrackerService # support any events. end - def find_issue(issue_key, rendered_fields: false) - options = {} - options = options.merge(expand: 'renderedFields') if rendered_fields + def find_issue(issue_key, rendered_fields: false, transitions: false) + expands = [] + expands << 'renderedFields' if rendered_fields + expands << 'transitions' if transitions + options = { expand: expands.join(',') } if expands.any? - jira_request { client.Issue.find(issue_key, options) } + jira_request { client.Issue.find(issue_key, options || {}) } end def close_issue(entity, external_issue, current_user) - issue = find_issue(external_issue.iid) + issue = find_issue(external_issue.iid, transitions: automatic_issue_transitions?) - return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? + return if issue.nil? || has_resolution?(issue) commit_id = case entity when Commit then entity.id @@ -260,24 +258,52 @@ class JiraService < IssueTrackerService end end + def automatic_issue_transitions? + jira_issue_transition_id.blank? + end + # jira_issue_transition_id can have multiple values split by , or ; # the issue is transitioned at the order given by the user # if any transition fails it will log the error message and stop the transition sequence def transition_issue(issue) - jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id| - issue.transitions.build.save!(transition: { id: transition_id }) - rescue => error - log_error( - "Issue transition failed", - error: { - exception_class: error.class.name, - exception_message: error.message, - exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) - }, - client_url: client_url - ) - return false + return transition_issue_to_done(issue) if automatic_issue_transitions? + + jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id| + transition_issue_to_id(issue, transition_id) + end + end + + def transition_issue_to_id(issue, transition_id) + issue.transitions.build.save!( + transition: { id: transition_id } + ) + + true + rescue => error + log_error( + "Issue transition failed", + error: { + exception_class: error.class.name, + exception_message: error.message, + exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) + }, + client_url: client_url + ) + + false + end + + def transition_issue_to_done(issue) + transitions = issue.transitions rescue [] + + transition = transitions.find do |transition| + status = transition&.to&.statusCategory + status && status['key'] == 'done' end + + return false unless transition + + transition_issue_to_id(issue, transition.id) end def log_usage(action, user) diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index e13ef7a3811..a2d25e425da 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -10,10 +10,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated include BlobHelper include ChecksCollaboration include Gitlab::Utils::StrongMemoize + include Gitlab::Experiment::Dsl presents :project - AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop) + AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data) MAX_TOPICS_TO_SHOW = 3 def statistic_icon(icon_name = 'plus-square-o') @@ -33,6 +34,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def statistics_buttons(show_auto_devops_callout:) [ + upload_anchor_data, readme_anchor_data, license_anchor_data, changelog_anchor_data, @@ -49,6 +51,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_buttons [ + upload_anchor_data, new_file_anchor_data, readme_anchor_data, license_anchor_data, @@ -154,6 +157,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def can_current_user_push_to_branch?(branch) + return false unless current_user + user_access(project).can_push_to_branch?(branch) end @@ -232,19 +237,47 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated empty_repo? ? nil : project_tags_path(project)) end + def upload_anchor_data + strong_memoize(:upload_anchor_data) do + next unless can_current_user_push_to_default_branch? + + experiment(:empty_repo_upload, project: project) do |e| + e.use {} + e.try do + AnchorData.new(false, + statistic_icon('upload') + _('Upload file'), + '#modal-upload-blob', + 'js-upload-file-experiment-trigger', + nil, + nil, + { + 'toggle' => 'modal', + 'target' => '#modal-upload-blob' + } + ) + end + e.run + end + end + end + + def empty_repo_upload_experiment? + upload_anchor_data.present? + end + def new_file_anchor_data - if current_user && can_current_user_push_to_default_branch? + if can_current_user_push_to_default_branch? new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master) AnchorData.new(false, statistic_icon + _('New file'), new_file_path, - 'dashed') + 'btn-dashed') end end def readme_anchor_data - if current_user && can_current_user_push_to_default_branch? && readme_path.nil? + if can_current_user_push_to_default_branch? && readme_path.nil? AnchorData.new(false, statistic_icon + _('Add README'), empty_repo? ? add_readme_ide_path : add_readme_path) @@ -252,13 +285,13 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('README'), default_view != 'readme' ? readme_path : '#readme', - 'default', + 'btn-default', 'doc-text') end end def changelog_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? + if can_current_user_push_to_default_branch? && repository.changelog.blank? AnchorData.new(false, statistic_icon + _('Add CHANGELOG'), empty_repo? ? add_changelog_ide_path : add_changelog_path) @@ -266,7 +299,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CHANGELOG'), changelog_path, - 'default') + 'btn-default') end end @@ -277,11 +310,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, icon + content_tag(:span, license_short_name, class: 'project-stat-value'), license_path, - 'default', + 'btn-default', nil, 'license') else - if current_user && can_current_user_push_to_default_branch? + if can_current_user_push_to_default_branch? AnchorData.new(false, content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'), empty_repo? ? add_license_ide_path : add_license_path) @@ -294,7 +327,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def contribution_guide_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? + if can_current_user_push_to_default_branch? && repository.contribution_guide.blank? AnchorData.new(false, statistic_icon + _('Add CONTRIBUTING'), empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path) @@ -302,7 +335,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CONTRIBUTING'), contribution_guide_path, - 'default') + 'btn-default') end end @@ -312,7 +345,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('settings') + _('Auto DevOps enabled'), project_settings_ci_cd_path(project, anchor: 'autodevops-settings'), - 'default') + 'btn-default') else AnchorData.new(false, statistic_icon + _('Enable Auto DevOps'), @@ -337,7 +370,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, _('Kubernetes'), cluster_link, - 'default') + 'btn-default') end end end @@ -351,7 +384,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CI/CD configuration'), ci_configuration_path, - 'default') + 'btn-default') end end diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index f400c7de5eb..b68c75701b9 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -17,7 +17,7 @@ %br .dropzone-alerts.gl-alert.gl-alert-danger.gl-mb-5.data{ style: "display:none" } - = render 'shared/new_commit_form', placeholder: placeholder + = render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref] .form-actions = button_tag class: 'btn gl-button btn-success btn-upload-file', id: 'submit-all', type: 'button' do diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 81c354f1c8f..92867fdf349 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,7 +2,6 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, "help-page-path" => help_page_path('ci/quick_start/README'), - "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "project-id": @project.id, diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 2c245c1a914..d16f271c8d8 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout -- default_branch_name = @project.default_branch || "master" +- default_branch_name = @project.default_branch_or_master - @skip_current_level_breadcrumb = true = content_for :invite_members_sidebar do @@ -77,3 +77,6 @@ %span>< git push -u origin --all git push -u origin --tags + +- if @project.empty_repo_upload_experiment? + = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, default_branch_name), ref: default_branch_name, method: :post diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6a4dd88ae07..70d67fa75b7 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -8,7 +8,6 @@ project_id: @project.id, params: params.to_json, "help-page-path" => help_page_path('ci/quick_start/README'), - "auto-devops-help-path" => help_page_path('topics/autodevops/index.md'), "pipeline-schedule-url" => pipeline_schedules_path(@project), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 62ba89e2576..5641c67e462 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -3,8 +3,11 @@ = render 'shared/commit_message_container', placeholder: placeholder -- if @project.empty_repo? - = hidden_field_tag 'branch_name', @ref +- if project.empty_repo? + - ref = local_assigns[:ref] || @ref + - branch_name_class = project.empty_repo_upload_experiment? ? 'js-branch-name' : nil + + = hidden_field_tag 'branch_name', ref, class: branch_name_class - else - if can?(current_user, :push_code, @project) .form-group.row.branch diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 2b6920ed80f..26986c913f0 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -43,6 +43,9 @@ - options[:dropdown_class] += ' dropdown-extended-height' - options[:footer_content] = true - options[:wrapper_class] = 'js-sidebar-assignee-dropdown' + - options[:toggle_class] += ' js-invite-members-track' + - data['track-event'] = show_invite_members_track_event + - options[:data].merge!(data) - invite_text = _('Invite Members') - track_label = 'edit_assignee' diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index ac55f883fc5..313b901c08e 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -123,6 +123,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker def after_project_changes_hooks(project, user, refs, changes) experiment(:new_project_readme, actor: user).track_initial_writes(project) + experiment(:empty_repo_upload, project: project).track(:initial_write) if project.empty_repo? repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs) SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks) Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) diff --git a/changelogs/unreleased/16119-jira-issue-transition-automatic.yml b/changelogs/unreleased/16119-jira-issue-transition-automatic.yml new file mode 100644 index 00000000000..7d2a7a7b6c0 --- /dev/null +++ b/changelogs/unreleased/16119-jira-issue-transition-automatic.yml @@ -0,0 +1,5 @@ +--- +title: Support automatic transitions of Jira issues +merge_request: 53760 +author: +type: changed diff --git a/changelogs/unreleased/221296-skip_orphaned_pool_repositories_on_restore.yml b/changelogs/unreleased/221296-skip_orphaned_pool_repositories_on_restore.yml new file mode 100644 index 00000000000..387fd59236d --- /dev/null +++ b/changelogs/unreleased/221296-skip_orphaned_pool_repositories_on_restore.yml @@ -0,0 +1,5 @@ +--- +title: Skip orphaned pool repositories on restore +merge_request: 54112 +author: +type: fixed diff --git a/changelogs/unreleased/297240-enable-skip_dag_manual_and_delayed_jobs.yml b/changelogs/unreleased/297240-enable-skip_dag_manual_and_delayed_jobs.yml new file mode 100644 index 00000000000..8c83a73fdfd --- /dev/null +++ b/changelogs/unreleased/297240-enable-skip_dag_manual_and_delayed_jobs.yml @@ -0,0 +1,5 @@ +--- +title: Fix not skipped manual and delayed DAG jobs +merge_request: 54073 +author: +type: fixed diff --git a/changelogs/unreleased/33685-lift-npm-package-naming-convention-for-project-level-api.yml b/changelogs/unreleased/33685-lift-npm-package-naming-convention-for-project-level-api.yml new file mode 100644 index 00000000000..0aedf325fcf --- /dev/null +++ b/changelogs/unreleased/33685-lift-npm-package-naming-convention-for-project-level-api.yml @@ -0,0 +1,5 @@ +--- +title: Lift the NPM package naming convention for the project level API +merge_request: 53266 +author: +type: changed diff --git a/changelogs/unreleased/ld-handle-gids-for-unknown-objects.yml b/changelogs/unreleased/ld-handle-gids-for-unknown-objects.yml new file mode 100644 index 00000000000..1b4cdc859ce --- /dev/null +++ b/changelogs/unreleased/ld-handle-gids-for-unknown-objects.yml @@ -0,0 +1,5 @@ +--- +title: Handle GlobalIDs with invalid resource names +merge_request: 54290 +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 7e15d02a6da..7860c999b65 100644 --- a/config/application.rb +++ b/config/application.rb @@ -184,7 +184,6 @@ module Gitlab config.assets.precompile << "page_bundles/build.css" config.assets.precompile << "page_bundles/ci_status.css" config.assets.precompile << "page_bundles/cycle_analytics.css" - config.assets.precompile << "page_bundles/security_discover.css" config.assets.precompile << "page_bundles/dev_ops_report.css" config.assets.precompile << "page_bundles/environments.css" config.assets.precompile << "page_bundles/epics.css" @@ -194,6 +193,7 @@ module Gitlab config.assets.precompile << "page_bundles/import.css" config.assets.precompile << "page_bundles/incident_management_list.css" config.assets.precompile << "page_bundles/issues_list.css" + config.assets.precompile << "page_bundles/iterations.css" config.assets.precompile << "page_bundles/jira_connect.css" config.assets.precompile << "page_bundles/jira_connect_users.css" config.assets.precompile << "page_bundles/merge_conflicts.css" @@ -208,6 +208,7 @@ module Gitlab config.assets.precompile << "page_bundles/reports.css" config.assets.precompile << "page_bundles/roadmap.css" config.assets.precompile << "page_bundles/security_dashboard.css" + config.assets.precompile << "page_bundles/security_discover.css" config.assets.precompile << "page_bundles/signup.css" config.assets.precompile << "page_bundles/terminal.css" config.assets.precompile << "page_bundles/todos.css" diff --git a/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml b/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml index 678a52ee613..640be201868 100644 --- a/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml +++ b/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297240 milestone: '13.8' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/experiment/empty_repo_upload.yml b/config/feature_flags/experiment/empty_repo_upload.yml new file mode 100644 index 00000000000..9655a4d3cb4 --- /dev/null +++ b/config/feature_flags/experiment/empty_repo_upload.yml @@ -0,0 +1,8 @@ +--- +name: empty_repo_upload +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52755 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285296 +milestone: '13.9' +type: experiment +group: group::adoption +default_enabled: false diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 0f26eb83d17..644e2d905ae 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -4,7 +4,7 @@ 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 --- -# Web terminals +# Web terminals **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7690) in GitLab 8.15. diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md index ac09acf2d2e..d2e5f40b171 100644 --- a/doc/administration/monitoring/index.md +++ b/doc/administration/monitoring/index.md @@ -11,9 +11,19 @@ Explore our features to monitor your GitLab instance: - [GitLab self-monitoring](gitlab_self_monitoring_project/index.md): The GitLab instance administration project helps to monitor the GitLab instance and take action on alerts. -- [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance. -- [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products. -- [GitHub imports](github_imports.md): Monitor the health and progress of the GitHub importer with various Prometheus metrics. -- [Monitoring uptime](../../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. - - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed. -- [`nginx_status`](https://docs.gitlab.com/omnibus/settings/nginx.html#enablingdisabling-nginx_status): Monitor your NGINX server status +- [Performance monitoring](performance/index.md): GitLab Performance Monitoring + makes it possible to measure a wide variety of statistics of your instance. +- [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring + service, providing a flexible platform for monitoring GitLab and other software + products. +- [GitHub imports](github_imports.md): Monitor the health and progress of the GitHub + importer with various Prometheus metrics. +- [Monitoring uptime](../../user/admin_area/monitoring/health_check.md): Check the + server status using the health check endpoint. + - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that + provide health check information when probed. +- [`nginx_status`](https://docs.gitlab.com/omnibus/settings/nginx.html#enablingdisabling-nginx_status): + Monitor your NGINX server status. +- [Auto Monitoring](../../topics/autodevops/stages.md#auto-monitoring): Automated + monitoring for your application’s server and response metrics, provided by + [Auto DevOps](../../topics/autodevops/index.md). diff --git a/doc/api/services.md b/doc/api/services.md index 16431937c7a..64daca1793a 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -4,7 +4,7 @@ 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 --- -# Services API +# Services API **(FREE)** NOTE: This API requires an access token with Maintainer or Owner permissions @@ -814,7 +814,7 @@ Parameters: | `username` | string | yes | The username of the user created to be used with GitLab/Jira. | | `password` | string | yes | The password of the user created to be used with GitLab/Jira. | | `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). | -| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the transitions ID column. By default, this ID is set to `2`. | +| `jira_issue_transition_id` | string | no | The ID of one or more transitions to move issues to a closed state. Read [custom issue transitions](../user/project/integrations/jira.md#custom-issue-transitions) for details. Defaults to a blank string, which enables [automatic issue transitions](../user/project/integrations/jira.md#automatic-issue-transitions). | | `commit_events` | boolean | false | Enable notifications for commit events | | `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) | diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 9de6a1162bd..b44526135a1 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -12,6 +12,10 @@ type: reference Review Apps is a collaboration tool that takes the hard work out of providing an environment to showcase product changes. +NOTE: +If you have a Kubernetes cluster, you can automate this feature in your applications +by using [Auto DevOps](../../topics/autodevops/index.md). + ## Introduction Review Apps: diff --git a/doc/development/auto_devops.md b/doc/development/auto_devops.md index eaf1d712f17..d11587a864d 100644 --- a/doc/development/auto_devops.md +++ b/doc/development/auto_devops.md @@ -47,8 +47,8 @@ found in ## Development environment -Configuring [GDK for Auto -DevOps](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md). +See the [Simple way to develop/test Kubernetes workflows with a local cluster](https://gitlab.com/gitlab-org/gitlab-development-kit/-/issues/1064) +issue for discussion around setting up Auto DevOps development environments. ## Monitoring on GitLab.com diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md index bfe523ee390..860fd88612f 100644 --- a/doc/development/integrations/jira_connect.md +++ b/doc/development/integrations/jira_connect.md @@ -4,7 +4,7 @@ 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 --- -# Set up a development environment +# Set up a development environment **(FREE)** The following are required to install and test the app: diff --git a/doc/integration/README.md b/doc/integration/README.md index 7d38dd6222a..9ae75019efb 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w comments: false --- -# GitLab integrations +# GitLab integrations **(FREE)** GitLab can be integrated with external services for enhanced functionality. diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index a2720c82fe7..d2e20b225cc 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -4,7 +4,7 @@ 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 --- -# Akismet +# Akismet **(FREE)** GitLab leverages [Akismet](https://akismet.com/) to protect against spam. GitLab uses Akismet to prevent the creation of spam issues on public projects. Issues diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index 7e531682faf..ecf54613ec6 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -4,7 +4,7 @@ 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 --- -# Auth0 OmniAuth Provider +# Auth0 OmniAuth Provider **(FREE)** To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an application. diff --git a/doc/integration/azure.md b/doc/integration/azure.md index c83ef650f54..16c8bb4a785 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -4,7 +4,7 @@ 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 --- -# Microsoft Azure OAuth2 OmniAuth Provider +# Microsoft Azure OAuth2 OmniAuth Provider **(FREE)** NOTE: Per Microsoft, this provider uses the [older Azure Active Directory v1.0 endpoint](https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-protocols-oauth-code). diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 8999f4da9a2..54bcaee7cab 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -4,7 +4,7 @@ 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 --- -# Integrate your GitLab server with Bitbucket Cloud +# Integrate your GitLab server with Bitbucket Cloud **(FREE)** NOTE: Starting from GitLab 11.4, OmniAuth is enabled by default. If you're using an diff --git a/doc/integration/cas.md b/doc/integration/cas.md index b444143b03e..59f41ab4368 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -4,7 +4,7 @@ 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 --- -# CAS OmniAuth Provider +# CAS OmniAuth Provider **(FREE)** To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab supplies to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for back-channel logout. diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 389c7aabdf5..9b25dd69e59 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -218,7 +218,7 @@ The following Elasticsearch settings are available: | `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 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/`). | +| `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/`). Special characters in the username or password should use [percentage encoding](https://en.wikipedia.org/wiki/Percent-encoding). | | `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). | | `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value will greatly increase total disk space required by the index. | | `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects). diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index b1eb9d0d2fe..4e00057c4b7 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -4,7 +4,7 @@ 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 --- -# External issue tracker +# External issue tracker **(FREE)** GitLab has a great [issue tracker](../user/project/issues/index.md) but you can also use an external one. External issue trackers are configurable per GitLab project. diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index b86958726a7..c901fdfdd10 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -4,7 +4,7 @@ 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 --- -# Facebook OAuth2 OmniAuth Provider +# Facebook OAuth2 OmniAuth Provider **(FREE)** To enable the Facebook OmniAuth provider you must register your application with Facebook. Facebook generates an app ID and secret key for you to use. diff --git a/doc/integration/github.md b/doc/integration/github.md index 0239ba0e818..026e4c99593 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -4,7 +4,7 @@ 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 --- -# Integrate your GitLab instance with GitHub +# Integrate your GitLab instance with GitHub **(FREE)** You can integrate your GitLab instance with GitHub.com and GitHub Enterprise. This integration enables users to import projects from GitHub, or sign in to your GitLab instance diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 97c5332c438..7e21685fd54 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -4,7 +4,7 @@ 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 --- -# Integrate your server with GitLab.com +# Integrate your server with GitLab.com **(FREE)** Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account. diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index af9972e825e..fa0e79d4c0b 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -4,7 +4,7 @@ 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 --- -# Gmail actions buttons for GitLab +# Gmail actions buttons for GitLab **(FREE)** GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). diff --git a/doc/integration/google.md b/doc/integration/google.md index cd00c854fea..0e4c9b78b5f 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -4,7 +4,7 @@ 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 --- -# Google OAuth2 OmniAuth Provider +# Google OAuth2 OmniAuth Provider **(FREE)** To enable the Google OAuth2 OmniAuth provider you must register your application with Google. Google generates a client ID and secret key for you to use. diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md index 2ed87456ea1..17446a3cde3 100644 --- a/doc/integration/jenkins_deprecated.md +++ b/doc/integration/jenkins_deprecated.md @@ -4,7 +4,7 @@ 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 --- -# Jenkins CI (deprecated) service +# Jenkins CI (deprecated) service **(FREE)** NOTE: In GitLab 8.3, Jenkins integration using the diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md index 41656bd2216..84490757c16 100644 --- a/doc/integration/oauth2_generic.md +++ b/doc/integration/oauth2_generic.md @@ -4,7 +4,7 @@ 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 --- -# Sign into GitLab with (almost) any OAuth2 provider +# Sign into GitLab with (almost) any OAuth2 provider **(FREE)** The `omniauth-oauth2-generic` gem allows Single Sign-On between GitLab and your own OAuth2 provider (or any OAuth2 provider compatible with this gem) diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index cf033018e17..9fd2e7a69e9 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -4,7 +4,7 @@ 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 --- -# OmniAuth +# OmniAuth **(FREE)** GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. [OmniAuth](https://rubygems.org/gems/omniauth/) is diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md index 4455ace3e1f..b37c5064063 100644 --- a/doc/integration/openid_connect_provider.md +++ b/doc/integration/openid_connect_provider.md @@ -4,7 +4,7 @@ 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 --- -# GitLab as OpenID Connect identity provider +# GitLab as OpenID Connect identity provider **(FREE)** This document is about using GitLab as an OpenID Connect identity provider to sign in to other services. diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md index 8f090dfc588..9ffc89e2c13 100644 --- a/doc/integration/recaptcha.md +++ b/doc/integration/recaptcha.md @@ -4,7 +4,7 @@ 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 --- -# reCAPTCHA +# reCAPTCHA **(FREE)** GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/about/) to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md index 156109518a6..102b89298a1 100644 --- a/doc/integration/salesforce.md +++ b/doc/integration/salesforce.md @@ -4,7 +4,7 @@ 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 --- -# Salesforce OmniAuth Provider +# Salesforce OmniAuth Provider **(FREE)** You can integrate your GitLab instance with [Salesforce](https://www.salesforce.com/) to enable users to log in to your GitLab instance with their Salesforce account. diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index 3b92f2858ac..c4cf19747be 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -4,7 +4,7 @@ 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 --- -# Shibboleth OmniAuth Provider +# Shibboleth OmniAuth Provider **(FREE)** NOTE: The preferred approach for integrating a Shibboleth authentication system diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 0dcf86cc46d..2c133c1de76 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -4,7 +4,7 @@ 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 --- -# Slash Commands +# Slash Commands **(FREE)** > - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24780) to GitLab Free in 11.9. diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index 1a1b6cd101f..e8956271508 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -4,7 +4,7 @@ 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 --- -# Trello Power-Up +# Trello Power-Up **(FREE)** The GitLab Trello Power-Up enables you to seamlessly attach GitLab **merge requests** to Trello cards. diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 8404352d0e9..58e111be73c 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -4,7 +4,7 @@ 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 --- -# Twitter OAuth2 OmniAuth Provider +# Twitter OAuth2 OmniAuth Provider **(FREE)** To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter generates a client ID and secret key for you to use. diff --git a/doc/update/index.md b/doc/update/index.md index a07a6f4b0b7..574f86865a1 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -147,7 +147,7 @@ Find where your version sits in the upgrade path below, and upgrade GitLab accordingly, while also consulting the [version-specific upgrade instructions](#version-specific-upgrading-instructions): -`8.11.x` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.1.17` -> `12.10.14` -> `13.0.14` -> `13.1.11` - > `13.x (latest)` +`8.11.Z` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.1.17` -> `12.10.14` -> `13.0.14` -> `13.1.11` - > [latest `13.Y.Z`](https://about.gitlab.com/releases/categories/releases/) The following table, while not exhaustive, shows some examples of the supported upgrade paths. @@ -174,7 +174,7 @@ Identify a [supported upgrade path](#upgrade-paths). More significant migrations may occur during major release upgrades. To ensure these are successful: -1. Increment to the first minor version (`x.0.x`) during the major version jump. +1. Increment to the first minor version (`X.0.Z`) during the major version jump. 1. Proceed with upgrading to a newer release. It's also important to ensure that any background migrations have been fully completed @@ -235,7 +235,7 @@ which is the latest patch release of 9.4. When GitLab 9.5.0 is released this installation can be safely upgraded to 9.5.0 without requiring downtime if the requirements mentioned above are met. You can also skip 9.5.0 and upgrade to 9.5.1 after it's released, but you **can not** upgrade straight to 9.6.0; you -_have_ to first upgrade to a 9.5.x release. +_have_ to first upgrade to a 9.5.Z release. **Example 2:** You are running a large GitLab installation using version 9.4.2, which is the latest patch release of 9.4. GitLab 9.5 includes some background @@ -243,7 +243,7 @@ migrations, and 10.0 will require these to be completed (processing any remaining jobs for you). Skipping 9.5 is not possible without downtime, and due to the background migrations would require potentially hours of downtime depending on how long it takes for the background migrations to complete. To -work around this you will have to upgrade to 9.5.x first, then wait at least a +work around this you will have to upgrade to 9.5.Z first, then wait at least a week before upgrading to 10.0. **Example 3:** You use MySQL as the database for GitLab. Any upgrade to a new @@ -361,9 +361,9 @@ with the older Rails version - which could cause non-GET requests to fail for [multi-node GitLab installations](https://docs.gitlab.com/omnibus/update/#multi-node--ha-deployment). So, if you are using multiple Rails servers and specifically upgrading from 13.0, -all servers must first be upgraded to 13.1.X before upgrading to 13.2.0 or later: +all servers must first be upgraded to 13.1.Z before upgrading to 13.2.0 or later: -1. Ensure all GitLab web nodes are on GitLab 13.1.X. +1. Ensure all GitLab web nodes are on GitLab 13.1.Z. 1. Optionally, enable the `global_csrf_token` feature flag to enable new method of CSRF token generation: @@ -383,33 +383,33 @@ any downgrades would result to all sessions being invalidated and users are logg ### 12.1.0 -If you are planning to upgrade from `12.0.x` to `12.10.x`, it is necessary to -perform an intermediary upgrade to `12.1.x` before upgrading to `12.10.x` to +If you are planning to upgrade from `12.0.Z` to `12.10.Z`, it is necessary to +perform an intermediary upgrade to `12.1.Z` before upgrading to `12.10.Z` to avoid issues like [#215141](https://gitlab.com/gitlab-org/gitlab/-/issues/215141). ### 12.0.0 In 12.0.0 we made various database related changes. These changes require that -users first upgrade to the latest 11.11 patch release. After upgraded to 11.11.x, -users can upgrade to 12.0.x. Failure to do so may result in database migrations +users first upgrade to the latest 11.11 patch release. After upgraded to 11.11.Z, +users can upgrade to 12.0.Z. Failure to do so may result in database migrations not being applied, which could lead to application errors. -It is also required that you upgrade to 12.0.x before moving to a later version -of 12.x. +It is also required that you upgrade to 12.0.Z before moving to a later version +of 12.Y. Example 1: you are currently using GitLab 11.11.8, which is the latest patch -release for 11.11.x. You can upgrade as usual to 12.0.x. +release for 11.11.Z. You can upgrade as usual to 12.0.Z. -Example 2: you are currently using a version of GitLab 10.x. To upgrade, first -upgrade to the last 10.x release (10.8.7) then the last 11.x release (11.11.8). -After upgraded to 11.11.8 you can safely upgrade to 12.0.x. +Example 2: you are currently using a version of GitLab 10.Y. To upgrade, first +upgrade to the last 10.Y release (10.8.7) then the last 11.Y release (11.11.8). +After upgraded to 11.11.8 you can safely upgrade to 12.0.Z. See our [documentation on upgrade paths](../policy/maintenance.md#upgrade-recommendations) for more information. ### Upgrades from versions earlier than 8.12 -- `8.11.x` and earlier: you might have to upgrade to `8.12.0` specifically before you can upgrade to `8.17.7`. This was [reported in an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/207259). +- `8.11.Z` and earlier: you might have to upgrade to `8.12.0` specifically before you can upgrade to `8.17.7`. This was [reported in an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/207259). - [CI changes prior to version 8.0](https://docs.gitlab.com/omnibus/update/README.html#updating-gitlab-ci-from-prior-540-to-version-714-via-omnibus-gitlab) when it was merged into GitLab. diff --git a/doc/user/admin_area/settings/project_integration_management.md b/doc/user/admin_area/settings/project_integration_management.md index 18491f92650..0b9f039880a 100644 --- a/doc/user/admin_area/settings/project_integration_management.md +++ b/doc/user/admin_area/settings/project_integration_management.md @@ -4,7 +4,7 @@ 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 --- -# Project integration management +# Project integration management **(FREE)** Project integrations can be configured and enabled by project administrators. As a GitLab instance administrator, you can set default configuration parameters for a given integration that all projects diff --git a/doc/user/analytics/img/mr_mean_time_to_merge_metric_v13_9.png b/doc/user/analytics/img/mr_mean_time_to_merge_metric_v13_9.png Binary files differnew file mode 100644 index 00000000000..ad108dabf73 --- /dev/null +++ b/doc/user/analytics/img/mr_mean_time_to_merge_metric_v13_9.png diff --git a/doc/user/analytics/merge_request_analytics.md b/doc/user/analytics/merge_request_analytics.md index 3edbe3e8aa4..909eb7e585f 100644 --- a/doc/user/analytics/merge_request_analytics.md +++ b/doc/user/analytics/merge_request_analytics.md @@ -39,6 +39,15 @@ Merge Request Analytics could be used when: The following visualizations and data are available, representing all merge requests that were merged in the given date range. +### Mean time to merge + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229389) in GitLab 13.9. + +The mean time to merge (MTTM) metric shows the average time between when a merge request is created, +and when it is merged. To view how the MTTM changes over time, compare MTTM across different date ranges. + +![Mean time to merge](img/mr_mean_time_to_merge_metric_v13_9.png "Merge Request Analytics - MTTM metric showing the average time it takes from initiating a MR to being merged") + ### Throughput chart The throughput chart shows the number of merge requests merged per month. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 6eec1418ef0..98aea2f6642 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -32,7 +32,7 @@ To integrate security scanners other than Clair and Klar into GitLab, see You can enable container scanning by doing one of the following: - [Include the CI job](#configuration) in your existing `.gitlab-ci.yml` file. -- Implicitly use [Auto Container Scanning](../../../topics/autodevops/stages.md#auto-container-scanning) +- Implicitly use [Auto Container Scanning](../../../topics/autodevops/stages.md#auto-container-scanning), provided by [Auto DevOps](../../../topics/autodevops/index.md). GitLab compares the found vulnerabilities between the source and target branches, and shows the diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 3950c856b40..0cd7cb95f26 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -24,10 +24,13 @@ organization. If you're using [GitLab CI/CD](../../../ci/README.md), you can analyze your running web applications for known vulnerabilities using Dynamic Application Security Testing (DAST). -You can take advantage of DAST by either [including the CI job](#configuration) in -your existing `.gitlab-ci.yml` file or by implicitly using -[Auto DAST](../../../topics/autodevops/stages.md#auto-dast), -provided by [Auto DevOps](../../../topics/autodevops/index.md). +You can take advantage of DAST by either: + +- [Including the CI job](#configuration) in + your existing `.gitlab-ci.yml` file. +- Implicitly using + [Auto DAST](../../../topics/autodevops/stages.md#auto-dast), + provided by [Auto DevOps](../../../topics/autodevops/index.md). GitLab checks the DAST report, compares the found vulnerabilities between the source and target branches, and shows the information on the merge request. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 11d27140e42..eab490d3800 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -19,10 +19,13 @@ vulnerable. You can then take action to protect your application. If you're using [GitLab CI/CD](../../../ci/README.md), you can use dependency scanning to analyze your dependencies for known vulnerabilities. GitLab scans all dependencies, including transitive dependencies (also known as nested dependencies). You can take advantage of dependency scanning by -either [including the dependency scanning template](#configuration) -in your existing `.gitlab-ci.yml` file, or by implicitly using -the [auto dependency scanning](../../../topics/autodevops/stages.md#auto-dependency-scanning) -provided by [Auto DevOps](../../../topics/autodevops/index.md). +either: + +- [Including the dependency scanning template](#configuration) + in your existing `.gitlab-ci.yml` file. +- Implicitly using + the [auto dependency scanning](../../../topics/autodevops/stages.md#auto-dependency-scanning) + provided by [Auto DevOps](../../../topics/autodevops/index.md). GitLab checks the dependency scanning report, compares the found vulnerabilities between the source and target branches, and shows the information on the diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 880edebfeda..4f06e63490d 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -149,7 +149,7 @@ The [Security Scanner Integration](../../../development/integrations/secure.md) To configure SAST for a project you can: -- Use [Auto SAST](../../../topics/autodevops/stages.md#auto-sast) provided by +- Use [Auto SAST](../../../topics/autodevops/stages.md#auto-sast), provided by [Auto DevOps](../../../topics/autodevops/index.md). - [Configure SAST manually](#configure-sast-manually). - [Configure SAST using the UI](#configure-sast-in-the-ui) (introduced in GitLab 13.3). diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md index 98177e804f3..9390a5def18 100644 --- a/doc/user/application_security/secret_detection/index.md +++ b/doc/user/application_security/secret_detection/index.md @@ -112,7 +112,7 @@ For example, `https://username:$password@example.com/path/to/repo` isn't detecte NOTE: You don't have to configure Secret Detection manually as shown in this section if you're using -[Auto Secret Detection](../../../topics/autodevops/stages.md#auto-secret-detection) +[Auto Secret Detection](../../../topics/autodevops/stages.md#auto-secret-detection), provided by [Auto DevOps](../../../topics/autodevops/index.md). To enable Secret Detection for GitLab 13.1 and later, you must include the diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 6f3d1e197f5..d8fddca4d09 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -21,6 +21,7 @@ tasks in a secure and cloud-native way. It enables: - Pull-based GitOps deployments by leveraging the [GitOps Engine](https://github.com/argoproj/gitops-engine). - Real-time access to API endpoints in a cluster. +- Alert generation based on [Container network policy](../../application_security/threat_monitoring/index.md#container-network-policy). Many more features are planned. Please review [our roadmap](https://gitlab.com/groups/gitlab-org/-/epics/3329) and [our development documentation](../../../development/agent/index.md). @@ -445,6 +446,51 @@ The following example projects can help you get started with the Kubernetes Agen You can use the Kubernetes Agent to [deploy GitLab Runner in a Kubernetes cluster](http://docs.gitlab.com/runner/install/kubernetes-agent.html). +## Kubernetes Network Security Alerts + +The GitLab Agent also provides an integration with Cilium. This integration provides a simple way to +generate network policy-related alerts and to surface those alerts in GitLab. + +There are several components that work in concert for the Agent to generate the alerts: + +- A working Kubernetes cluster. +- Cilium integration through either of these options: + - Installation through [GitLab Managed Apps](../applications.md#install-cilium-using-gitlab-cicd). + - Enablement of [hubble-relay](https://docs.cilium.io/en/v1.8/concepts/overview/#hubble) on an + existing installation. +- One or more network policies through any of these options: + - Use the [Container Network Policy editor](../../application_security/threat_monitoring/index.md#container-network-policy-editor) to create and manage policies. + - Use an [AutoDevOps](../../application_security/threat_monitoring/index.md#container-network-policy-management) configuration. + - Add the required labels and annotations to existing network policies. +- Use a configuration repository to inform the Agent through a `config.yaml` file, which + repositories can synchronize with. This repository might be the same, or a separate GitLab + project. + +The setup process follows the same steps as [GitOps](#get-started-with-gitops-and-the-gitlab-agent), +with the following differences: + +- When you define a configuration repository, you must do so with [Cilium settings](#define-a-configuration-repository-with-cilium-settings). +- You do not need to create a `manifest.yaml`. + +### Define a configuration repository with Cilium settings + +You need a GitLab repository to contain your Agent configuration. The minimal repository layout +looks like this: + +```plaintext +.gitlab/agents/<agent-name>/config.yaml +``` + +Your `config.yaml` file must specify the `host` and `port` of your Hubble Relay service. If your +Cilium integration was performed through [GitLab Managed Apps](../applications.md#install-cilium-using-gitlab-cicd), +you can use `hubble-relay.gitlab-managed-apps.svc.cluster.local:80`: + +```yaml +cilium: + hubble_relay_address: "<hubble-relay-host>:<hubble-relay-port>" + ... +``` + ## Management interfaces Users with at least the [Developer](../../permissions.md) can access the user interface diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 74c608fcc38..31d3f7e1db0 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -14,10 +14,13 @@ project's dependencies for their licenses. You can then decide whether to allow each license. For example, if your application uses an external (open source) library whose license is incompatible with yours, then you can deny the use of that license. -You can take advantage of License Compliance by either [including the job](#configuration) -in your existing `.gitlab-ci.yml` file or by implicitly using -[Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance) -that is provided by [Auto DevOps](../../../topics/autodevops/index.md). +You can take advantage of License Compliance by either: + +- [Including the job](#configuration) + in your existing `.gitlab-ci.yml` file. +- Implicitly using + [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance), + provided by [Auto DevOps](../../../topics/autodevops/index.md). The [License Finder](https://github.com/pivotal/LicenseFinder) scan tool runs as part of the CI/CD pipeline, and detects the licenses in use. GitLab checks the License Compliance report, compares the diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index d1c490b0769..24216188c80 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -87,7 +87,7 @@ Please note that the certificate [fingerprint algorithm](#additional-providers-a With this option enabled, users must go through your group's GitLab single sign-on URL. They may also be added via SCIM, if configured. Users can't be added manually, and may only access project/group resources via the UI by signing in through the SSO URL. However, users are not prompted to sign in through SSO on each visit. GitLab checks whether a user -has authenticated through SSO. If it's been more than 7 days since the last sign-in, GitLab +has authenticated through SSO. If it's been more than 1 day since the last sign-in, GitLab prompts the user to sign in again through SSO. You can see more information about how long a session is valid in our [user profile documentation](../../profile/#why-do-i-keep-getting-signed-out). diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index b1075e19b7b..f64b86c7200 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -86,7 +86,9 @@ A `package.json` file is created. To use the GitLab endpoint for npm packages, choose an option: - **Project-level**: Use when you have few npm packages and they are not in - the same GitLab group. + the same GitLab group. The [package naming convention](#package-naming-convention) is not enforced at this level. + Instead, you should use a [scope](https://docs.npmjs.com/cli/v6/using-npm/scope) for your package. + When you use a scope, the registry URL is [updated](#authenticate-to-the-package-registry) only for that scope. - **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace. Be sure to comply with the [package naming convention](#package-naming-convention). @@ -204,7 +206,7 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD. ## Package naming convention -Your npm package name must be in the format of `@scope/package-name`. +When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available. - The `@scope` is the root namespace of the GitLab project. It must match exactly, including the case. - The `package-name` can be whatever you want. @@ -302,8 +304,9 @@ the same version more than once, even if it has been deleted. ## Install a package npm packages are commonly-installed by using the `npm` or `yarn` commands -in a JavaScript project. You can install a package from the scope of a project, group, -or instance. +in a JavaScript project. You can install a package from the scope of a project or instance. + +If multiple packages have the same name and version, when you install a package, the most recently-published package is retrieved. 1. Set the URL for scoped packages by running: diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 464edf13a7e..c9b17eaaefe 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -58,12 +58,10 @@ Dark mode is available as a navigation theme, for MVC and compatibility reasons. the future, we plan to make it configurable in its own section along with support for [different navigation themes](https://gitlab.com/gitlab-org/gitlab/-/issues/219512). -NOTE: Dark theme only works with the **Dark** syntax highlighting theme. ## Syntax highlighting theme -NOTE: GitLab uses the [rouge Ruby library](http://rouge.jneen.net/ "Rouge website") for syntax highlighting outside of any Editor context. The WebIDE (like Snippets) uses [Monaco Editor](https://microsoft.github.io/monaco-editor/) and it's provided @@ -88,11 +86,7 @@ The default syntax theme is White, and you can choose among 5 different themes: ![Profile preferences syntax highlighting themes](img/profile-preferences-syntax-themes.png) -[Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in 13.0, the theme -you choose also applies to the [Web IDE](../project/web_ide/index.md)'s code editor and [Snippets](../snippets.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and -the [Solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), -which apply to the entire Web IDE screen. +Introduced in GitLab 13.6, the themes [Solarized](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) also apply to the [Web IDE](../project/web_ide/index.md) and [Snippets](../snippets.md). ## Behavior diff --git a/doc/user/project/code_intelligence.md b/doc/user/project/code_intelligence.md index 0e8c1bf8f4d..4d783c7dddd 100644 --- a/doc/user/project/code_intelligence.md +++ b/doc/user/project/code_intelligence.md @@ -19,6 +19,9 @@ Code Intelligence is built into GitLab and powered by [LSIF](https://lsif.dev/) (Language Server Index Format), a file format for precomputed code intelligence data. +NOTE: +You can automate this feature in your applications by using [Auto DevOps](../../topics/autodevops/index.md). + ## Configuration Enable code intelligence for a project by adding a GitLab CI/CD job to the project's diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md index 72695580d9d..83e3d0c2c80 100644 --- a/doc/user/project/deploy_boards.md +++ b/doc/user/project/deploy_boards.md @@ -16,6 +16,10 @@ of the pods in the deployment. Developers and other teammates can view the progress and status of a rollout, pod by pod, in the workflow they already use without any need to access Kubernetes. +NOTE: +If you have a Kubernetes cluster, you can Auto Deploy applications to production +environments by using [Auto DevOps](../../topics/autodevops/index.md). + ## Overview With Deploy Boards you can gain more insight into deploys with benefits such as: diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index fb9314f7504..3b012ab4430 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -4,7 +4,7 @@ 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 --- -# Atlassian Bamboo CI Service +# Atlassian Bamboo CI Service **(FREE)** GitLab provides integration with Atlassian Bamboo for continuous integration. When configured, pushes to a project trigger a build in Bamboo automatically. diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 4e2ee9b3662..7e14515d98e 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -4,7 +4,7 @@ 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 --- -# Bugzilla Service +# Bugzilla Service **(FREE)** Navigate to the [Integrations page](overview.md#accessing-integrations), select the **Bugzilla** service and fill in the required details as described diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 143f0e2a25d..9cc4e980212 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -4,7 +4,7 @@ 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 --- -# Custom Issue Tracker service +# Custom Issue Tracker service **(FREE)** To enable the Custom Issue Tracker integration in a project: diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index 8e0a167a968..624c0252f23 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -4,7 +4,7 @@ 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 --- -# Discord Notifications service +# Discord Notifications service **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22684) in GitLab 11.6. diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index 2274913d349..4970e20974b 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -4,7 +4,7 @@ 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 --- -# Enabling emails on push +# Enabling emails on push **(FREE)** By enabling this service, you receive email notifications for every change that is pushed to your project. diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index 06dcca6eb44..d0efebd4da7 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -4,7 +4,7 @@ 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 --- -# Hangouts Chat service +# Hangouts Chat service **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/43756) in GitLab 11.2. diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md index 7b90d8d7cfd..f66fc0a0f6a 100644 --- a/doc/user/project/integrations/hipchat.md +++ b/doc/user/project/integrations/hipchat.md @@ -4,7 +4,7 @@ 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 --- -# Atlassian HipChat +# Atlassian HipChat **(FREE)** GitLab provides a way to send HipChat notifications upon a number of events, such as when a user pushes code, creates a branch or tag, adds a comment, and diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 0e5163e992a..5628a9bc5e5 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -4,7 +4,7 @@ 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 --- -# Project integrations +# Project integrations **(FREE)** You can find the available integrations under your project's **Settings > Integrations** page. You need to have at least diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 58f7ea3279f..e75561b3ddb 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -4,7 +4,7 @@ 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 --- -# Irker IRC Gateway +# Irker IRC Gateway **(FREE)** GitLab provides a way to push update messages to an Irker server. When configured, pushes to a project trigger the service to send data directly diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 5857c3da803..050f4faa5aa 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -4,7 +4,7 @@ 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 --- -# GitLab Jira integration +# GitLab Jira integration **(FREE)** You can use Jira to track work implemented in GitLab. The Jira integration with GitLab makes the process of working across these systems more efficient. @@ -106,8 +106,6 @@ To enable the Jira integration in a project: 1. To include a comment on the Jira issue when the above reference is made in GitLab, select **Enable comments**. - 1. Select the **Comment detail**: **Standard** or **All details**. - 1. Enter the further details on the page as described in the following table. | Field | Description | @@ -116,7 +114,6 @@ To enable the Jira integration in a project: | `Jira API URL` | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira on Atlassian cloud**. | | `Username or Email` | Created in [configure Jira](#configure-jira) step. Use `username` for **Jira Server** or `email` for **Jira on Atlassian cloud**. | | `Password/API token` | Created in [configure Jira](#configure-jira) step. Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. | - | `Jira workflow transition IDs` | Required for closing Jira issues via commits or merge requests. These are the IDs of transitions in Jira that move issues to a particular state. (See [Obtaining a transition ID](#obtaining-a-transition-id).) If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. In GitLab 13.6 and earlier, field was called `Transition ID`. | 1. To enable users to view Jira issues inside the GitLab project, select **Enable Jira issues** and enter a Jira project key. **(PREMIUM)** @@ -138,10 +135,19 @@ To enable the Jira integration in a project: Your GitLab project can now interact with all Jira projects in your instance and the project now displays a Jira link that opens the Jira project. -#### Obtaining a transition ID +#### Automatic issue transitions + +When you [close a Jira issues with a trigger word](../issues/managing_issues.md#closing-issues-automatically), +GitLab by default transitions the issue to the next available status with a category of "Done". + +#### Custom issue transitions + +For advanced workflows you can specify custom Jira transition IDs. If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. + +To see the transition IDs on Jira Cloud, edit a workflow in the **Text** view. +The transition IDs display in the **Transitions** column. -In the most recent Jira user interface, you can no longer see transition IDs in the workflow -administration UI. You can get the ID you need in either of the following ways: +On Jira Server you can get the transition IDs in either of the following ways: 1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions` using an issue that is in the appropriate "open" state diff --git a/doc/user/project/integrations/jira_cloud_configuration.md b/doc/user/project/integrations/jira_cloud_configuration.md index e9f30f32308..8e25af3f884 100644 --- a/doc/user/project/integrations/jira_cloud_configuration.md +++ b/doc/user/project/integrations/jira_cloud_configuration.md @@ -4,7 +4,7 @@ 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 --- -# Create an API token in Jira on Atlassian cloud +# Create an API token in Jira on Atlassian cloud **(FREE)** For [integrations with Jira](jira.md), an API token is needed when integrating with Jira on Atlassian cloud. To create an API token: diff --git a/doc/user/project/integrations/jira_integrations.md b/doc/user/project/integrations/jira_integrations.md index 6a1529f001a..6b938238320 100644 --- a/doc/user/project/integrations/jira_integrations.md +++ b/doc/user/project/integrations/jira_integrations.md @@ -4,7 +4,7 @@ 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 --- -# Jira integrations +# Jira integrations **(FREE)** GitLab can be integrated with [Jira](https://www.atlassian.com/software/jira). diff --git a/doc/user/project/integrations/jira_server_configuration.md b/doc/user/project/integrations/jira_server_configuration.md index 5bb17388a1e..b1ab2076dc0 100644 --- a/doc/user/project/integrations/jira_server_configuration.md +++ b/doc/user/project/integrations/jira_server_configuration.md @@ -4,7 +4,7 @@ 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 --- -# Create Jira Server username and password +# Create Jira Server username and password **(FREE)** For [integrations with Jira](jira.md), you must create a user account in Jira to have access to all projects that need to integrate with GitLab. diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index db190f47b01..6a93fc0fb06 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -4,7 +4,7 @@ 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 --- -# Mattermost Notifications Service +# Mattermost Notifications Service **(FREE)** The Mattermost Notifications Service allows your GitLab project to send events (e.g., `issue created`) to your existing Mattermost team as notifications. This requires configurations in both Mattermost and GitLab. diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 6c8a0ded2ae..d2ef6a24ddf 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -4,7 +4,7 @@ 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 --- -# Mattermost slash commands +# Mattermost slash commands **(FREE)** > Introduced in GitLab 8.14 diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index 136da05d0e8..41e0938fc3b 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -4,7 +4,7 @@ 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 --- -# Microsoft Teams service +# Microsoft Teams service **(FREE)** ## On Microsoft Teams diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index 18f0ad6b275..934510fd155 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -4,7 +4,7 @@ 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 --- -# Mock CI Service +# Mock CI Service **(FREE)** **NB: This service is only listed if you are in a development environment!** diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index b22a7c0295e..f6590b6ba2f 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -4,7 +4,7 @@ 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 --- -# Integrations +# Integrations **(FREE)** Integrations allow you to integrate GitLab with other applications. They are a bit like plugins in that they allow a lot of freedom in adding diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 38d6194b390..256ffe84ee2 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -4,7 +4,7 @@ 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 --- -# Redmine Service +# Redmine Service **(FREE)** 1. To enable the Redmine integration in a project, navigate to the [Integrations page](overview.md#accessing-integrations), click diff --git a/doc/user/project/integrations/servicenow.md b/doc/user/project/integrations/servicenow.md index 1de69f60a34..bdc05552c31 100644 --- a/doc/user/project/integrations/servicenow.md +++ b/doc/user/project/integrations/servicenow.md @@ -4,7 +4,7 @@ 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 --- -# ServiceNow integration +# ServiceNow integration **(FREE)** ServiceNow offers several integrations to help centralize and automate your management of GitLab workflows. diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md index 7507792bb02..66810d8a01b 100644 --- a/doc/user/project/integrations/services_templates.md +++ b/doc/user/project/integrations/services_templates.md @@ -4,7 +4,7 @@ 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 --- -# Service templates +# Service templates **(FREE)** WARNING: Service templates are [deprecated and scheduled to be removed](https://gitlab.com/gitlab-org/gitlab/-/issues/268032) diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index ab798675278..17d1c3adcb5 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -4,7 +4,7 @@ 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 --- -# Slack Notifications Service +# Slack Notifications Service **(FREE)** The Slack Notifications Service allows your GitLab project to send events (such as issue creation) to your existing Slack team as notifications. Setting up diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index e8dcb577aba..3e5e368722e 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -4,7 +4,7 @@ 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 --- -# Unify Circuit service +# Unify Circuit service **(FREE)** The Unify Circuit service sends notifications from GitLab to the conversation for which the webhook was created. diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md index 8a3383fd0e7..6820412808f 100644 --- a/doc/user/project/integrations/webex_teams.md +++ b/doc/user/project/integrations/webex_teams.md @@ -4,7 +4,7 @@ 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 --- -# Webex Teams service +# Webex Teams service **(FREE)** You can configure GitLab to send notifications to a Webex Teams space. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 0cf01adef13..63f15b7b914 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -4,7 +4,7 @@ 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 --- -# Webhooks +# Webhooks **(FREE)** Project webhooks allow you to trigger a percent-encoded URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index f9b3c083a54..f9f61de9d6b 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -4,7 +4,7 @@ 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 --- -# YouTrack Service +# YouTrack Service **(FREE)** JetBrains [YouTrack](https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Documentation.html) is a web-based issue tracking and project management platform. diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md index 6fa2340c7a4..f6575ba4173 100644 --- a/doc/user/project/merge_requests/browser_performance_testing.md +++ b/doc/user/project/merge_requests/browser_performance_testing.md @@ -13,6 +13,9 @@ If your application offers a web interface and you're using [GitLab CI/CD](../../../ci/README.md), you can quickly determine the rendering performance impact of pending code changes in the browser. +NOTE: +You can automate this feature in your applications by using [Auto DevOps](../../../topics/autodevops/index.md). + ## Overview GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 55dc0bcc41a..451561cc22f 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -22,8 +22,7 @@ Code Quality: [GitLab Code Quality](https://gitlab.com/gitlab-org/ci-cd/codequality) project using [default Code Climate configurations](https://gitlab.com/gitlab-org/ci-cd/codequality/-/tree/master/codeclimate_defaults). - Can make use of a [template](#example-configuration). -- Is available with [Auto - DevOps](../../../topics/autodevops/stages.md#auto-code-quality). +- Is available by using [Auto Code Quality](../../../topics/autodevops/stages.md#auto-code-quality), provided by [Auto DevOps](../../../topics/autodevops/index.md). - Can be extended through [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) or a [custom tool](#implementing-a-custom-tool). ## Code Quality Widget diff --git a/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png Binary files differindex ccb9cf6f126..8eca352a4d0 100644 --- a/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png +++ b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png Binary files differdeleted file mode 100644 index 6257d78d29e..00000000000 --- a/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png +++ /dev/null diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 07f46cb94f7..5345a031c48 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -71,19 +71,16 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io). ### Themes -> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0. +> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab 13.0. > - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1. +> - Full [Solarized Light](https://gitlab.com/gitlab-org/gitlab/-/issues/221035) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) Themes introduced in GitLab 13.6. -All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor. +All the themes GitLab supports for syntax highlighting are applied to the Web IDE's entire screen. You can pick a theme from your [profile preferences](../../profile/preferences.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and -the [Solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), -which apply to the entire Web IDE screen. - -| Solarized Light Theme | Solarized Dark Theme | Dark Theme | -|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------| -| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) | +| Solarized Dark Theme | Dark Theme | +|-------------------------------------------------------------|-----------------------------------------| +| ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) | ## Schema based validation diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb index 833288c6013..d6e006df976 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -41,8 +41,8 @@ module API authorize_read_package!(project) - packages = ::Packages::Npm::PackageFinder.new(project, package_name) - .execute + packages = ::Packages::Npm::PackageFinder.new(package_name, project: project) + .execute not_found! if packages.empty? @@ -68,9 +68,8 @@ module API authorize_create_package!(project) - package = ::Packages::Npm::PackageFinder - .new(project, package_name) - .find_by_version(version) + package = ::Packages::Npm::PackageFinder.new(package_name, project: project) + .find_by_version(version) not_found!('Package') unless package ::Packages::Npm::CreateTagService.new(package, tag).execute @@ -112,9 +111,8 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do package_name = params[:package_name] - - packages = ::Packages::Npm::PackageFinder.new(project_or_nil, package_name) - .execute + packages = ::Packages::Npm::PackageFinder.new(package_name, project: project_or_nil) + .execute redirect_request = project_or_nil.blank? || packages.empty? diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb index c1f6a001201..b6edec3f143 100644 --- a/lib/api/helpers/packages/npm.rb +++ b/lib/api/helpers/packages/npm.rb @@ -49,13 +49,34 @@ module API when :project params[:id] when :instance - ::Packages::Package.npm - .with_name(params[:package_name]) - .first - &.project_id + namespace_path = namespace_path_from_package_name + next unless namespace_path + + namespace = namespace_from_path(namespace_path) + next unless namespace + + finder = ::Packages::Npm::PackageFinder.new(params[:package_name], namespace: namespace) + + finder.last&.project_id end end end + + # from "@scope/package-name" return "scope" or nil + def namespace_path_from_package_name + package_name = params[:package_name] + return unless package_name.starts_with?('@') + return unless package_name.include?('/') + + package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first + end + + def namespace_from_path(path) + group = Group.by_path(path) + return group if group + + Namespace.for_user.by_path(path) + end end end end diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb index d15114a72a3..79b7b2c61f2 100644 --- a/lib/backup/repositories.rb +++ b/lib/backup/repositories.rb @@ -201,7 +201,12 @@ module Backup PoolRepository.includes(:source_project).find_each do |pool| progress.puts " - Object pool #{pool.disk_path}..." - pool.source_project ||= pool.member_projects.first.root_of_fork_network + pool.source_project ||= pool.member_projects.first&.root_of_fork_network + unless pool.source_project + progress.puts " - Object pool #{pool.disk_path}... " + "[SKIPPED]".color(:cyan) + next + end + pool.state = 'none' pool.save diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 96f2b7570b3..00739c05386 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -61,6 +61,10 @@ module Gitlab maven_app_name_regex end + def npm_package_name_regex + @npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z} + end + def nuget_package_name_regex @nuget_package_name_regex ||= %r{\A[-+\.\_a-zA-Z0-9]+\z}.freeze end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 78238a42f56..09a2dc6b5c3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7291,6 +7291,9 @@ msgstr "" msgid "Collapse approvers" msgstr "" +msgid "Collapse issues" +msgstr "" + msgid "Collapse milestones" msgstr "" @@ -12068,6 +12071,9 @@ msgstr "" msgid "Expand file" msgstr "" +msgid "Expand issues" +msgstr "" + msgid "Expand milestones" msgstr "" @@ -16669,6 +16675,9 @@ msgstr "" msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgstr "" +msgid "Issues with label %{label}" +msgstr "" + msgid "Issues with no epic assigned" msgstr "" @@ -16813,6 +16822,9 @@ msgstr "" msgid "JiraService|An error occured while fetching issue list" msgstr "" +msgid "JiraService|Automatically transitions Jira issues to the \"Done\" category. %{linkStart}Learn more%{linkEnd}" +msgstr "" + msgid "JiraService|Define the type of Jira issue to create from a vulnerability." msgstr "" @@ -16867,7 +16879,7 @@ msgstr "" msgid "JiraService|Jira project key" msgstr "" -msgid "JiraService|Jira workflow transition IDs" +msgid "JiraService|Move to Done" msgstr "" msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}." @@ -16888,7 +16900,7 @@ msgstr "" msgid "JiraService|Select issue type" msgstr "" -msgid "JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}" +msgid "JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}" msgstr "" msgid "JiraService|Sign in to GitLab.com to get started." @@ -16900,12 +16912,18 @@ msgstr "" msgid "JiraService|This issue is synchronized with Jira" msgstr "" +msgid "JiraService|Transition Jira issues to their final state:" +msgstr "" + msgid "JiraService|Use a password for server version and an API token for cloud version" msgstr "" msgid "JiraService|Use a username for server version and an email for cloud version" msgstr "" +msgid "JiraService|Use custom transitions" +msgstr "" + msgid "JiraService|Username or Email" msgstr "" @@ -17248,6 +17266,9 @@ msgstr "" msgid "Labels can be applied to issues and merge requests." msgstr "" +msgid "Labels with no issues in this iteration:" +msgstr "" + msgid "Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}" msgstr "" @@ -20145,6 +20166,9 @@ msgstr "" msgid "No issues found" msgstr "" +msgid "No issues found for the selected labels" +msgstr "" + msgid "No iteration" msgstr "" diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 18676807fd6..1a25c9349da 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -224,8 +224,7 @@ module QA end def merge! - wait_until_ready_to_merge - click_element(:merge_button) + try_to_merge! finished_loading? raise "Merge did not appear to be successful" unless merged? @@ -237,7 +236,11 @@ module QA end def merged? - has_element?(:merged_status_content, text: 'The changes were merged into', wait: 60) + # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042 + # To remove page refresh logic if possible + retry_until(max_attempts: 3, reload: true) do + has_element?(:merged_status_content, text: 'The changes were merged into', wait: 20) + end end # Check if the MR is able to be merged @@ -280,7 +283,10 @@ module QA end def try_to_merge! + # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042 + # To remove page refresh logic if possible wait_until_ready_to_merge + wait_until { !find_element(:merge_button).has_text?("when pipeline succeeds") } click_element(:merge_button) end diff --git a/qa/qa/page/project/settings/services/jira.rb b/qa/qa/page/project/settings/services/jira.rb index eaa3e90db78..a9409d0223a 100644 --- a/qa/qa/page/project/settings/services/jira.rb +++ b/qa/qa/page/project/settings/services/jira.rb @@ -10,7 +10,12 @@ module QA element :service_url_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern element :service_username_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern element :service_password_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern - element :service_jira_issue_transition_id_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern + end + + view 'app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue' do + element :service_issue_transition_mode_auto, ':data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern + element :service_issue_transition_mode_custom, ':data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern + element :service_jira_issue_transition_id_field end view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do @@ -23,7 +28,9 @@ module QA set_jira_server_url(url) set_username(Runtime::Env.jira_admin_username) set_password(Runtime::Env.jira_admin_password) - set_transaction_ids('11,21,31,41') + + use_custom_transitions + set_transition_ids('11,21,31,41') click_save_changes_button wait_until(reload: false) do @@ -45,8 +52,16 @@ module QA fill_element(:service_password_field, password) end - def set_transaction_ids(transaction_ids) - fill_element(:service_jira_issue_transition_id_field, transaction_ids) + def use_automatic_transitions + click_element :service_issue_transition_mode_auto + end + + def use_custom_transitions + click_element :service_issue_transition_mode_custom + end + + def set_transition_ids(transition_ids) + fill_element(:service_jira_issue_transition_id_field, transition_ids) end def click_save_changes_button diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1e4ec48b119..0c23d3f59e3 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -221,6 +221,20 @@ RSpec.describe ProjectsController do allow(controller).to receive(:record_experiment_user) end + context 'when user can push to default branch' do + let(:user) { empty_project.owner } + + it 'creates an "view_project_show" experiment tracking event', :snowplow do + allow_next_instance_of(ApplicationExperiment) do |e| + allow(e).to receive(:should_track?).and_return(true) + end + + get :show, params: { namespace_id: empty_project.namespace, id: empty_project } + + expect_snowplow_event(category: 'empty_repo_upload', action: 'view_project_show', context: [{ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', data: anything }]) + end + end + User.project_views.keys.each do |project_view| context "with #{project_view} view set" do before do diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb index 85afc54be48..3b8032e1d0d 100644 --- a/spec/features/projects/services/user_activates_jira_spec.rb +++ b/spec/features/projects/services/user_activates_jira_spec.rb @@ -6,12 +6,14 @@ RSpec.describe 'User activates Jira', :js do include_context 'project service activation' include_context 'project service Jira context' + before do + server_info = { key: 'value' }.to_json + stub_request(:get, test_url).to_return(body: server_info) + end + describe 'user tests Jira Service' do context 'when Jira connection test succeeds' do before do - server_info = { key: 'value' }.to_json - stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info) - visit_project_integration('Jira') fill_form click_test_then_save_integration(expect_test_to_fail: false) @@ -81,4 +83,40 @@ RSpec.describe 'User activates Jira', :js do end end end + + describe 'issue transition settings' do + it 'shows validation errors' do + visit_project_integration('Jira') + + expect(page).to have_field('Move to Done', checked: true) + + fill_form + choose 'Use custom transitions' + click_save_integration + + within '[data-testid="issue-transition-settings"]' do + expect(page).to have_content('This field is required.') + end + + fill_in 'service[jira_issue_transition_id]', with: '1, 2, 3' + click_save_integration + + expect(page).to have_content('Jira settings saved and active.') + expect(project.reload.jira_service.jira_issue_transition_id).to eq('1, 2, 3') + end + + it 'clears the transition IDs when using automatic transitions' do + create(:jira_service, project: project, jira_issue_transition_id: '1, 2, 3') + visit_project_integration('Jira') + + expect(page).to have_field('Use custom transitions', checked: true) + expect(page).to have_field('service[jira_issue_transition_id]', with: '1, 2, 3') + + choose 'Move to Done' + click_save_integration + + expect(page).to have_content('Jira settings saved and active.') + expect(project.reload.jira_service.jira_issue_transition_id).to eq('') + end + end end diff --git a/spec/features/projects/show/user_uploads_files_spec.rb b/spec/features/projects/show/user_uploads_files_spec.rb index 053598a528e..b7c5d324d93 100644 --- a/spec/features/projects/show/user_uploads_files_spec.rb +++ b/spec/features/projects/show/user_uploads_files_spec.rb @@ -33,4 +33,34 @@ RSpec.describe 'Projects > Show > User uploads files' do include_examples 'it uploads and commit a new file to a forked project' end + + context 'with an empty repo' do + let(:project) { create(:project, :empty_repo, creator: user) } + + context 'when in the empty_repo_upload experiment' do + before do + stub_experiments(empty_repo_upload: :candidate) + + visit(project_path(project)) + end + + it 'uploads and commits a new text file', :js do + click_link('Upload file') + + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'New commit message') + end + + click_button('Upload file') + + wait_for_requests + + expect(page).to have_content('New commit message') + expect(page).to have_content('Lorem ipsum dolor sit amet') + expect(page).to have_content('Sed ut perspiciatis unde omnis') + end + end + end end diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb index 78c23971f92..f021d800f31 100644 --- a/spec/finders/packages/npm/package_finder_spec.rb +++ b/spec/finders/packages/npm/package_finder_spec.rb @@ -2,39 +2,139 @@ require 'spec_helper' RSpec.describe ::Packages::Npm::PackageFinder do - let(:package) { create(:npm_package) } + let_it_be_with_reload(:project) { create(:project)} + let_it_be(:package) { create(:npm_package, project: project) } + let(:project) { package.project } let(:package_name) { package.name } - describe '#execute!' do - subject { described_class.new(project, package_name).execute } + shared_examples 'accepting a namespace for' do |example_name| + before do + project.update!(namespace: namespace) + end + + context 'that is a group' do + let_it_be(:namespace) { create(:group) } + + it_behaves_like example_name + + context 'within another group' do + let_it_be(:subgroup) { create(:group, parent: namespace) } + + before do + project.update!(namespace: subgroup) + end + + it_behaves_like example_name + end + end + + context 'that is a user namespace' do + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { user.namespace } + + it_behaves_like example_name + end + end + + describe '#execute' do + shared_examples 'finding packages by name' do + it { is_expected.to eq([package]) } + + context 'with unknown package name' do + let(:package_name) { 'baz' } + + it { is_expected.to be_empty } + end + end + + subject { finder.execute } + + context 'with a project' do + let(:finder) { described_class.new(package_name, project: project) } - it { is_expected.to eq([package]) } + it_behaves_like 'finding packages by name' - context 'with unknown package name' do - let(:package_name) { 'baz' } + context 'set to nil' do + let(:project) { nil } - it { is_expected.to be_empty } + it { is_expected.to be_empty } + end end - context 'with nil project' do - let(:project) { nil } + context 'with a namespace' do + let(:finder) { described_class.new(package_name, namespace: namespace) } + + it_behaves_like 'accepting a namespace for', 'finding packages by name' + + context 'set to nil' do + let_it_be(:namespace) { nil } - it { is_expected.to be_empty } + it { is_expected.to be_empty } + end end end describe '#find_by_version' do let(:version) { package.version } - subject { described_class.new(project, package.name).find_by_version(version) } + subject { finder.find_by_version(version) } + + shared_examples 'finding packages by version' do + it { is_expected.to eq(package) } + + context 'with unknown version' do + let(:version) { 'foobar' } + + it { is_expected.to be_nil } + end + end + + context 'with a project' do + let(:finder) { described_class.new(package_name, project: project) } + + it_behaves_like 'finding packages by version' + end + + context 'with a namespace' do + let(:finder) { described_class.new(package_name, namespace: namespace) } + + it_behaves_like 'accepting a namespace for', 'finding packages by version' + end + end + + describe '#last' do + subject { finder.last } + + shared_examples 'finding package by last' do + it { is_expected.to eq(package) } + end + + context 'with a project' do + let(:finder) { described_class.new(package_name, project: project) } + + it_behaves_like 'finding package by last' + end + + context 'with a namespace' do + let(:finder) { described_class.new(package_name, namespace: namespace) } + + it_behaves_like 'accepting a namespace for', 'finding package by last' - it { is_expected.to eq(package) } + context 'with duplicate packages' do + let_it_be(:namespace) { create(:group) } + let_it_be(:subgroup1) { create(:group, parent: namespace) } + let_it_be(:subgroup2) { create(:group, parent: namespace) } + let_it_be(:project2) { create(:project, namespace: subgroup2) } + let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) } - context 'with unknown version' do - let(:version) { 'foobar' } + before do + project.update!(namespace: subgroup1) + end - it { is_expected.to be_nil } + # the most recent one is returned + it { is_expected.to eq(package2) } + end end end end diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js index cbd36abd4ff..95520577e9a 100644 --- a/spec/frontend/blob/blob_file_dropzone_spec.js +++ b/spec/frontend/blob/blob_file_dropzone_spec.js @@ -1,5 +1,10 @@ import $ from 'jquery'; import BlobFileDropzone from '~/blob/blob_file_dropzone'; +import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment'; + +jest.mock('~/projects/upload_file_experiment', () => ({ + trackUploadFileFormSubmitted: jest.fn(), +})); describe('BlobFileDropzone', () => { preloadFixtures('blob/show.html'); @@ -41,5 +46,13 @@ describe('BlobFileDropzone', () => { expect(replaceFileButton.is(':disabled')).toEqual(true); expect(dropzone.processQueue).toHaveBeenCalled(); }); + + it('calls the tracking event', () => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + replaceFileButton.click(); + + expect(trackUploadFileFormSubmitted).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js index f8bdd00f5da..8c442a0376e 100644 --- a/spec/frontend/commit/pipelines/pipelines_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_spec.js @@ -16,7 +16,6 @@ describe('Pipelines table in Commits and Merge requests', () => { helpPagePath: 'foo', emptyStateSvgPath: 'foo', errorStateSvgPath: 'foo', - autoDevopsHelpPath: 'foo', }; preloadFixtures(jsonFixtureName); diff --git a/spec/frontend/experiment_tracking_spec.js b/spec/frontend/experiment_tracking_spec.js new file mode 100644 index 00000000000..ac05419cecd --- /dev/null +++ b/spec/frontend/experiment_tracking_spec.js @@ -0,0 +1,77 @@ +import ExperimentTracking from '~/experiment_tracking'; +import Tracking from '~/tracking'; + +jest.mock('~/tracking'); + +const oldGon = window.gon; + +let experimentTracking; +let label; +let newGon = {}; + +const setup = () => { + window.gon = newGon; + experimentTracking = new ExperimentTracking('sidebar_experiment', label); +}; + +beforeEach(() => { + document.body.dataset.page = 'issues-page'; +}); + +afterEach(() => { + window.gon = oldGon; + Tracking.mockClear(); + label = undefined; +}); + +describe('event', () => { + describe('when experiment data exists for experimentName', () => { + beforeEach(() => { + newGon = { global: { experiment: { sidebar_experiment: 'experiment-data' } } }; + setup(); + }); + + describe('when providing options', () => { + label = { label: 'sidebar-drawer' }; + + it('passes them to the tracking call', () => { + experimentTracking.event('click_sidebar_close'); + + expect(Tracking.event).toHaveBeenCalledTimes(1); + expect(Tracking.event).toHaveBeenCalledWith('issues-page', 'click_sidebar_close', { + label: 'sidebar-drawer', + context: { + schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0', + data: 'experiment-data', + }, + }); + }); + }); + + it('tracks with the correct context', () => { + experimentTracking.event('click_sidebar_trigger'); + + expect(Tracking.event).toHaveBeenCalledTimes(1); + expect(Tracking.event).toHaveBeenCalledWith('issues-page', 'click_sidebar_trigger', { + label: undefined, + context: { + schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0', + data: 'experiment-data', + }, + }); + }); + }); + + describe('when experiment data does NOT exists for the experimentName', () => { + beforeEach(() => { + newGon = { global: { experiment: { unrelated_experiment: 'not happening' } } }; + setup(); + }); + + it('does not track', () => { + experimentTracking.event('click_sidebar_close'); + + expect(Tracking.event).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js index c6e7ee44355..6a8ab02a69a 100644 --- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js @@ -30,14 +30,21 @@ describe('JiraTriggerFields', () => { const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]'); const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]'); const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox); + const findIssueTransitionSettings = () => + wrapper.find('[data-testid="issue-transition-settings"]'); + const findIssueTransitionModeRadios = () => + findIssueTransitionSettings().findAll('input[type="radio"]'); + const findIssueTransitionIdsField = () => + wrapper.find('input[type="text"][name="service[jira_issue_transition_id]"]'); describe('template', () => { describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => { - it('does not show comment settings', () => { + it('does not show trigger settings', () => { createComponent(); expect(findCommentSettings().isVisible()).toBe(false); expect(findCommentDetail().isVisible()).toBe(false); + expect(findIssueTransitionSettings().isVisible()).toBe(false); }); }); @@ -48,9 +55,10 @@ describe('JiraTriggerFields', () => { }); }); - it('shows comment settings', () => { + it('shows trigger settings', () => { expect(findCommentSettings().isVisible()).toBe(true); expect(findCommentDetail().isVisible()).toBe(false); + expect(findIssueTransitionSettings().isVisible()).toBe(true); }); // As per https://vuejs.org/v2/guide/forms.html#Checkbox-1, @@ -73,13 +81,14 @@ describe('JiraTriggerFields', () => { }); describe('initialTriggerMergeRequest is true', () => { - it('shows comment settings', () => { + it('shows trigger settings', () => { createComponent({ initialTriggerMergeRequest: true, }); expect(findCommentSettings().isVisible()).toBe(true); expect(findCommentDetail().isVisible()).toBe(false); + expect(findIssueTransitionSettings().isVisible()).toBe(true); }); }); @@ -95,7 +104,41 @@ describe('JiraTriggerFields', () => { }); }); - it('disables checkboxes and radios if inheriting', () => { + describe('initialJiraIssueTransitionId is not set', () => { + it('uses automatic transitions', () => { + createComponent({ + initialTriggerCommit: true, + }); + + const [radio1, radio2] = findIssueTransitionModeRadios().wrappers; + expect(radio1.element.checked).toBe(true); + expect(radio2.element.checked).toBe(false); + + expect(findIssueTransitionIdsField().exists()).toBe(false); + }); + }); + + describe('initialJiraIssueTransitionId is set', () => { + it('uses custom transitions', () => { + createComponent({ + initialJiraIssueTransitionId: '1, 2, 3', + initialTriggerCommit: true, + }); + + const [radio1, radio2] = findIssueTransitionModeRadios().wrappers; + expect(radio1.element.checked).toBe(false); + expect(radio2.element.checked).toBe(true); + + const field = findIssueTransitionIdsField(); + expect(field.isVisible()).toBe(true); + expect(field.element).toMatchObject({ + type: 'text', + value: '1, 2, 3', + }); + }); + }); + + it('disables input fields if inheriting', () => { createComponent( { initialTriggerCommit: true, @@ -104,12 +147,8 @@ describe('JiraTriggerFields', () => { true, ); - wrapper.findAll('[type=checkbox]').wrappers.forEach((checkbox) => { - expect(checkbox.attributes('disabled')).toBe('disabled'); - }); - - wrapper.findAll('[type=radio]').wrappers.forEach((radio) => { - expect(radio.attributes('disabled')).toBe('disabled'); + wrapper.findAll('[type=text], [type=checkbox], [type=radio]').wrappers.forEach((input) => { + expect(input.attributes('disabled')).toBe('disabled'); }); }); }); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 44c9def99cc..4997e9cf3ec 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,9 +1,8 @@ import { shallowMount } from '@vue/test-utils'; -import $ from 'jquery'; import { trimText } from 'helpers/text_helper'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; -$.fn.popover = () => {}; +const projectPath = 'test/test'; describe('Pipeline Url Component', () => { let wrapper; @@ -14,6 +13,7 @@ describe('Pipeline Url Component', () => { const findYamlTag = () => wrapper.find('[data-testid="pipeline-url-yaml"]'); const findFailureTag = () => wrapper.find('[data-testid="pipeline-url-failure"]'); const findAutoDevopsTag = () => wrapper.find('[data-testid="pipeline-url-autodevops"]'); + const findAutoDevopsTagLink = () => wrapper.find('[data-testid="pipeline-url-autodevops-link"]'); const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]'); const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]'); const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]'); @@ -23,9 +23,9 @@ describe('Pipeline Url Component', () => { pipeline: { id: 1, path: 'foo', + project: { full_path: `/${projectPath}` }, flags: {}, }, - autoDevopsHelpPath: 'foo', pipelineScheduleUrl: 'foo', }; @@ -33,7 +33,7 @@ describe('Pipeline Url Component', () => { wrapper = shallowMount(PipelineUrlComponent, { propsData: { ...defaultProps, ...props }, provide: { - targetProjectFullPath: 'test/test', + targetProjectFullPath: projectPath, }, }); }; @@ -57,6 +57,19 @@ describe('Pipeline Url Component', () => { expect(findPipelineUrlLink().text()).toBe('#1'); }); + it('should not render tags when flags are not set', () => { + createComponent(); + + expect(findStuckTag().exists()).toBe(false); + expect(findLatestTag().exists()).toBe(false); + expect(findYamlTag().exists()).toBe(false); + expect(findAutoDevopsTag().exists()).toBe(false); + expect(findFailureTag().exists()).toBe(false); + expect(findScheduledTag().exists()).toBe(false); + expect(findForkTag().exists()).toBe(false); + expect(findTrainTag().exists()).toBe(false); + }); + it('should render the stuck tag when flag is provided', () => { createComponent({ pipeline: { @@ -96,6 +109,7 @@ describe('Pipeline Url Component', () => { it('should render an autodevops badge when flag is provided', () => { createComponent({ pipeline: { + ...defaultProps.pipeline, flags: { auto_devops: true, }, @@ -103,6 +117,11 @@ describe('Pipeline Url Component', () => { }); expect(trimText(findAutoDevopsTag().text())).toBe('Auto DevOps'); + + expect(findAutoDevopsTagLink().attributes()).toMatchObject({ + href: '/help/topics/autodevops/index.md', + target: '_blank', + }); }); it('should render a detached badge when flag is provided', () => { @@ -147,7 +166,7 @@ describe('Pipeline Url Component', () => { createComponent({ pipeline: { flags: {}, - project: { fullPath: 'test/forked' }, + project: { fullPath: '/test/forked' }, }, }); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 811303a5624..663bd3f6174 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -34,7 +34,6 @@ describe('Pipelines', () => { let origWindowLocation; const paths = { - autoDevopsHelpPath: '/help/topics/autodevops/index.md', helpPagePath: '/help/ci/quick_start/README', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', @@ -45,7 +44,6 @@ describe('Pipelines', () => { }; const noPermissions = { - autoDevopsHelpPath: '/help/topics/autodevops/index.md', helpPagePath: '/help/ci/quick_start/README', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index 660651547fc..60318511be2 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -9,7 +9,6 @@ describe('Pipelines Table Row', () => { mount(PipelinesTableRowComponent, { propsData: { pipeline, - autoDevopsHelpPath: 'foo', viewType: 'root', }, }); diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index fd73d507919..64febd7311c 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -9,7 +9,6 @@ describe('Pipelines Table', () => { const defaultProps = { pipelines: [], - autoDevopsHelpPath: 'foo', viewType: 'root', }; @@ -58,7 +57,7 @@ describe('Pipelines Table', () => { describe('with data', () => { it('should render rows', () => { - createComponent({ pipelines: [pipeline], autoDevopsHelpPath: 'foo', viewType: 'root' }); + createComponent({ pipelines: [pipeline], viewType: 'root' }); expect(findRows()).toHaveLength(1); }); diff --git a/spec/frontend/projects/upload_file_experiment_spec.js b/spec/frontend/projects/upload_file_experiment_spec.js new file mode 100644 index 00000000000..57abce779a5 --- /dev/null +++ b/spec/frontend/projects/upload_file_experiment_spec.js @@ -0,0 +1,46 @@ +import ExperimentTracking from '~/experiment_tracking'; +import * as UploadFileExperiment from '~/projects/upload_file_experiment'; + +const mockExperimentTrackingEvent = jest.fn(); +jest.mock('~/experiment_tracking', () => + jest.fn().mockImplementation(() => ({ + event: mockExperimentTrackingEvent, + })), +); + +const fixture = `<a class='js-upload-file-experiment-trigger' data-toggle='modal' data-target='#modal-upload-blob'></a><div id='modal-upload-blob'></div>`; +const findModal = () => document.querySelector('[aria-modal="true"]'); +const findTrigger = () => document.querySelector('.js-upload-file-experiment-trigger'); + +beforeEach(() => { + ExperimentTracking.mockClear(); + mockExperimentTrackingEvent.mockClear(); + + document.body.innerHTML = fixture; +}); + +afterEach(() => { + document.body.innerHTML = ''; +}); + +describe('trackUploadFileFormSubmitted', () => { + it('initializes ExperimentTracking with the correct arguments and calls the tracking event with correct arguments', () => { + UploadFileExperiment.trackUploadFileFormSubmitted(); + + expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', { + label: 'blob-upload-modal', + }); + expect(mockExperimentTrackingEvent).toHaveBeenCalledWith('click_upload_modal_form_submit'); + }); +}); + +describe('initUploadFileTrigger', () => { + it('calls modal and tracks event', () => { + UploadFileExperiment.initUploadFileTrigger(); + + expect(findModal()).not.toExist(); + findTrigger().click(); + expect(findModal()).toExist(); + expect(mockExperimentTrackingEvent).toHaveBeenCalledWith('click_upload_modal_trigger'); + }); +}); diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb index cb129868f7e..8eb023ad2a3 100644 --- a/spec/graphql/types/global_id_type_spec.rb +++ b/spec/graphql/types/global_id_type_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe Types::GlobalIDType do let_it_be(:project) { create(:project) } let(:gid) { project.to_global_id } - let(:foreign_gid) { GlobalID.new(::URI::GID.build(app: 'otherapp', model_name: 'Project', model_id: project.id, params: nil)) } it 'is has the correct name' do expect(described_class.to_graphql.name).to eq('GlobalID') @@ -41,16 +40,18 @@ RSpec.describe Types::GlobalIDType do it 'rejects invalid input' do expect { described_class.coerce_isolated_input('not valid') } - .to raise_error(GraphQL::CoercionError) + .to raise_error(GraphQL::CoercionError, /not a valid Global ID/) end it 'rejects nil' do expect(described_class.coerce_isolated_input(nil)).to be_nil end - it 'rejects gids from different apps' do - expect { described_class.coerce_isolated_input(foreign_gid) } - .to raise_error(GraphQL::CoercionError) + it 'rejects GIDs from different apps' do + invalid_gid = GlobalID.new(::URI::GID.build(app: 'otherapp', model_name: 'Project', model_id: project.id, params: nil)) + + expect { described_class.coerce_isolated_input(invalid_gid) } + .to raise_error(GraphQL::CoercionError, /is not a Gitlab Global ID/) end end @@ -79,14 +80,22 @@ RSpec.describe Types::GlobalIDType do let(:gid) { build_stubbed(:user).to_global_id } it 'raises errors when coercing results' do - expect { type.coerce_isolated_result(gid) }.to raise_error(GraphQL::CoercionError) + expect { type.coerce_isolated_result(gid) } + .to raise_error(GraphQL::CoercionError, /Expected a Project ID/) end it 'will not coerce invalid input, even if its a valid GID' do expect { type.coerce_isolated_input(gid.to_s) } - .to raise_error(GraphQL::CoercionError) + .to raise_error(GraphQL::CoercionError, /does not represent an instance of Project/) end end + + it 'handles GIDs for invalid resource names gracefully' do + invalid_gid = GlobalID.new(::URI::GID.build(app: GlobalID.app, model_name: 'invalid', model_id: 1, params: nil)) + + expect { type.coerce_isolated_input(invalid_gid) } + .to raise_error(GraphQL::CoercionError, /does not represent an instance of Project/) + end end describe 'a parameterized type with a namespace' do diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index 576021b37b3..ef6bc864eef 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -11,6 +11,21 @@ RSpec.describe InviteMembersHelper do helper.extend(Gitlab::Experimentation::ControllerConcern) end + describe '#show_invite_members_track_event' do + it 'shows values when can directly invite members' do + allow(helper).to receive(:directly_invite_members?).and_return(true) + + expect(helper.show_invite_members_track_event).to eq 'show_invite_members' + end + + it 'shows values when can indirectly invite members' do + allow(helper).to receive(:directly_invite_members?).and_return(false) + allow(helper).to receive(:indirectly_invite_members?).and_return(true) + + expect(helper.show_invite_members_track_event).to eq 'show_invite_members_version_b' + end + end + context 'with project' do before do assign(:project, project) diff --git a/spec/helpers/services_helper_spec.rb b/spec/helpers/services_helper_spec.rb index 534f33d9b5a..3b08393ebdf 100644 --- a/spec/helpers/services_helper_spec.rb +++ b/spec/helpers/services_helper_spec.rb @@ -4,32 +4,49 @@ require 'spec_helper' RSpec.describe ServicesHelper do describe '#integration_form_data' do + let(:fields) do + [ + :id, + :show_active, + :activated, + :type, + :merge_request_events, + :commit_events, + :enable_comments, + :comment_detail, + :learn_more_path, + :trigger_events, + :fields, + :inherit_from_id, + :integration_level, + :editable, + :cancel_path, + :can_test, + :test_path, + :reset_path + ] + end + + let(:jira_fields) { %i[jira_issue_transition_id] } + subject { helper.integration_form_data(integration) } - context 'Jira service' do - let(:integration) { build(:jira_service) } + context 'Slack service' do + let(:integration) { build(:slack_service) } - it 'includes Jira specific fields' do - is_expected.to include( - :id, - :show_active, - :activated, - :type, - :merge_request_events, - :commit_events, - :enable_comments, - :comment_detail, - :trigger_events, - :fields, - :inherit_from_id, - :integration_level - ) - end + it { is_expected.to include(*fields) } + it { is_expected.not_to include(*jira_fields) } specify do expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration)) end end + + context 'Jira service' do + let(:integration) { build(:jira_service) } + + it { is_expected.to include(*fields, *jira_fields) } + end end describe '#scoped_reset_integration_path' do diff --git a/spec/helpers/stat_anchors_helper_spec.rb b/spec/helpers/stat_anchors_helper_spec.rb index 0615baac3cb..f3830bf4172 100644 --- a/spec/helpers/stat_anchors_helper_spec.rb +++ b/spec/helpers/stat_anchors_helper_spec.rb @@ -18,7 +18,7 @@ RSpec.describe StatAnchorsHelper do context 'when anchor is not a link' do context 'when class_modifier is set' do - let(:anchor) { anchor_klass.new(false, nil, nil, 'default') } + let(:anchor) { anchor_klass.new(false, nil, nil, 'btn-default') } it 'returns the proper attributes' do expect(subject[:class]).to include('gl-button btn btn-default') @@ -49,5 +49,21 @@ RSpec.describe StatAnchorsHelper do expect(subject[:itemprop]).to eq true end end + + context 'when data is not set' do + let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, nil, nil) } + + it 'returns the data attributes' do + expect(subject[:data]).to be_nil + end + end + + context 'when itemprop is set' do + let(:anchor) { anchor_klass.new(false, nil, nil, nil, nil, nil, { 'toggle' => 'modal' }) } + + it 'returns the data attributes' do + expect(subject[:data]).to eq({ 'toggle' => 'modal' }) + end + end end end diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index 492058c6a00..7a8cc713e4f 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -230,6 +230,16 @@ RSpec.describe Backup::Repositories do expect(pool_repository).not_to be_failed expect(pool_repository.object_pool.exists?).to be(true) end + + it 'skips pools with no source project, :sidekiq_might_not_need_inline' do + pool_repository = create(:pool_repository, state: :obsolete) + pool_repository.update_column(:source_project_id, nil) + + subject.restore + + pool_repository.reload + expect(pool_repository).to be_obsolete + end end it 'cleans existing repositories' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 776ca81a338..1aca3dae41b 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -367,6 +367,35 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('%2e%2e%2f1.2.3') } end + describe '.npm_package_name_regex' do + subject { described_class.npm_package_name_regex } + + it { is_expected.to match('@scope/package') } + it { is_expected.to match('unscoped-package') } + it { is_expected.not_to match('@first-scope@second-scope/package') } + it { is_expected.not_to match('scope-without-at-symbol/package') } + it { is_expected.not_to match('@not-a-scoped-package') } + it { is_expected.not_to match('@scope/sub/package') } + it { is_expected.not_to match('@scope/../../package') } + it { is_expected.not_to match('@scope%2e%2e%2fpackage') } + it { is_expected.not_to match('@%2e%2e%2f/package') } + + context 'capturing group' do + [ + ['@scope/package', 'scope'], + ['unscoped-package', nil], + ['@not-a-scoped-package', nil], + ['@scope/sub/package', nil], + ['@inv@lid-scope/package', nil] + ].each do |package_name, extracted_scope_name| + it "extracts the scope name for #{package_name}" do + match = package_name.match(described_class.npm_package_name_regex) + expect(match&.captures&.first).to eq(extracted_scope_name) + end + end + end + end + describe '.nuget_version_regex' do subject { described_class.nuget_version_regex } diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 6c55d37b95f..483edb6b742 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -162,6 +162,18 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('../../../my_package').for(:name) } it { is_expected.not_to allow_value('%2e%2e%2fmy_package').for(:name) } end + + context 'npm package' do + subject { build_stubbed(:npm_package) } + + it { is_expected.to allow_value("@group-1/package").for(:name) } + it { is_expected.to allow_value("@any-scope/package").for(:name) } + it { is_expected.to allow_value("unscoped-package").for(:name) } + it { is_expected.not_to allow_value("@inv@lid-scope/package").for(:name) } + it { is_expected.not_to allow_value("@scope/../../package").for(:name) } + it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) } + it { is_expected.not_to allow_value("@scope/sub/package").for(:name) } + end end describe '#version' do @@ -342,16 +354,6 @@ RSpec.describe Packages::Package, type: :model do end describe '#package_already_taken' do - context 'npm package' do - let!(:package) { create(:npm_package) } - - it 'will not allow a package of the same name' do - new_package = build(:npm_package, project: create(:project), name: package.name) - - expect(new_package).not_to be_valid - end - end - context 'maven package' do let!(:package) { create(:maven_package) } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 78bd0e91208..8c948c7889f 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -82,11 +82,8 @@ RSpec.describe JiraService do subject(:fields) { service.fields } - it 'includes transition help link' do - transition_id_field = fields.find { |field| field[:name] == 'jira_issue_transition_id' } - - expect(transition_id_field[:title]).to eq('Jira workflow transition IDs') - expect(transition_id_field[:help]).to include('/help/user/project/integrations/jira') + it 'returns custom fields' do + expect(fields.pluck(:name)).to eq(%w[url api_url username password]) end end @@ -460,10 +457,10 @@ RSpec.describe JiraService do end context 'with options' do - let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields" } + let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields,transitions" } it 'calls the Jira API with the options to get the issue' do - jira_service.find_issue(issue_key, rendered_fields: true) + jira_service.find_issue(issue_key, rendered_fields: true, transitions: true) expect(WebMock).to have_requested(:get, issue_url) end @@ -474,6 +471,25 @@ RSpec.describe JiraService do let(:custom_base_url) { 'http://custom_url' } shared_examples 'close_issue' do + let(:issue_key) { 'JIRA-123' } + let(:issue_url) { "http://jira.example.com/rest/api/2/issue/#{issue_key}" } + let(:transitions_url) { "#{issue_url}/transitions" } + let(:comment_url) { "#{issue_url}/comment" } + let(:remote_link_url) { "#{issue_url}/remotelink" } + let(:transitions) { nil } + + let(:issue_fields) do + { + id: issue_key, + self: issue_url, + transitions: transitions + } + end + + subject(:close_issue) do + @jira_service.close_issue(resource, ExternalIssue.new(issue_key, project)) + end + before do @jira_service = described_class.new allow(@jira_service).to receive_messages( @@ -488,26 +504,21 @@ RSpec.describe JiraService do # These stubs are needed to test JiraService#close_issue. # We close the issue then do another request to API to check if it got closed. # Here is stubbed the API return with a closed and an opened issues. - open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { 'id' => 'JIRA-123' }) + open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: issue_fields.deep_stringify_keys) closed_issue = open_issue.dup allow(open_issue).to receive(:resolution).and_return(false) allow(closed_issue).to receive(:resolution).and_return(true) allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) - allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123') + allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return(issue_key) allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) @jira_service.save! - project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' - @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' - @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment' - @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - - WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:get, issue_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) end let(:external_issue) { ExternalIssue.new('JIRA-123', project) } @@ -519,7 +530,7 @@ RSpec.describe JiraService do it 'calls Jira API' do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: /Issue solved with/ ).once end @@ -546,9 +557,9 @@ RSpec.describe JiraService do favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" # Creates comment - expect(WebMock).to have_requested(:post, @comment_url) + expect(WebMock).to have_requested(:post, comment_url) # Creates Remote Link in Jira issue fields - expect(WebMock).to have_requested(:post, @remote_link_url).with( + expect(WebMock).to have_requested(:post, remote_link_url).with( body: hash_including( GlobalID: 'GitLab', relationship: 'mentioned on', @@ -567,8 +578,8 @@ RSpec.describe JiraService do allow(@jira_service).to receive_messages(comment_on_event_enabled: false) close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).to have_requested(:post, remote_link_url) end end @@ -589,7 +600,7 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, comment_url) end end @@ -598,8 +609,8 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).not_to have_requested(:post, remote_link_url) end it 'does not send comment or remote links to issues with unknown resolution' do @@ -607,8 +618,8 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).not_to have_requested(:post, remote_link_url) end it 'references the GitLab commit' do @@ -616,7 +627,7 @@ RSpec.describe JiraService do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: %r{#{custom_base_url}/#{project.full_path}/-/commit/#{commit_id}} ).once end @@ -631,14 +642,14 @@ RSpec.describe JiraService do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/#{commit_id}} ).once end it 'logs exception when transition id is not valid' do allow(@jira_service).to receive(:log_error) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") + WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") close_issue @@ -655,20 +666,63 @@ RSpec.describe JiraService do it 'calls the api with jira_issue_transition_id' do close_issue - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /999/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"999"/ ).once end - context 'when have multiple transition ids' do + context 'when using automatic issue transitions' do + let(:transitions) do + [ + { id: '1' }, + { id: '2', to: { statusCategory: { key: 'new' } } }, + { id: '3', to: { statusCategory: { key: 'done' } } }, + { id: '4', to: { statusCategory: { key: 'done' } } } + ] + end + + before do + allow(@jira_service).to receive_messages(jira_issue_transition_id: '') + + close_issue + end + + it 'uses the next transition with a status category of done' do + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"3"/ + ).once + end + + context 'when no done transition is available' do + let(:transitions) do + [ + { id: '1', to: { statusCategory: { key: 'new' } } } + ] + end + + it 'does not attempt to transition' do + expect(WebMock).not_to have_requested(:post, transitions_url) + end + end + + context 'when no valid transitions are returned' do + let(:transitions) { 'foo' } + + it 'does not attempt to transition' do + expect(WebMock).not_to have_requested(:post, transitions_url) + end + end + end + + context 'when using multiple transition ids' do it 'calls the api with transition ids separated by comma' do allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3') close_issue 1.upto(3) do |transition_id| - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /#{transition_id}/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"#{transition_id}"/ ).once end end @@ -679,8 +733,8 @@ RSpec.describe JiraService do close_issue 1.upto(3) do |transition_id| - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /#{transition_id}/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"#{transition_id}"/ ).once end end diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 98bcbd8384b..f78aa61529a 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -183,6 +183,14 @@ RSpec.describe ProjectPresenter do context 'not empty repo' do let(:project) { create(:project, :repository) } + context 'if no current user' do + let(:user) { nil } + + it 'returns false' do + expect(presenter.can_current_user_push_code?).to be(false) + end + end + it 'returns true if user can push to default branch' do project.add_developer(user) @@ -350,7 +358,7 @@ RSpec.describe ProjectPresenter do is_link: false, label: a_string_including("New file"), link: presenter.project_new_blob_path(project, 'master'), - class_modifier: 'dashed' + class_modifier: 'btn-dashed' ) end @@ -597,10 +605,12 @@ RSpec.describe ProjectPresenter do context 'for a developer' do before do project.add_developer(user) + stub_experiments(empty_repo_upload: :candidate) end it 'orders the items correctly' do expect(empty_repo_statistics_buttons.map(&:label)).to start_with( + a_string_including('Upload'), a_string_including('New'), a_string_including('README'), a_string_including('LICENSE'), @@ -609,6 +619,16 @@ RSpec.describe ProjectPresenter do a_string_including('CI/CD') ) end + + context 'when not in the upload experiment' do + before do + stub_experiments(empty_repo_upload: :control) + end + + it 'does not include upload button' do + expect(empty_repo_statistics_buttons.map(&:label)).not_to start_with(a_string_including('Upload')) + end + end end end @@ -694,4 +714,20 @@ RSpec.describe ProjectPresenter do end end end + + describe 'empty_repo_upload_experiment?' do + subject { presenter.empty_repo_upload_experiment? } + + it 'returns false when upload_anchor_data is nil' do + allow(presenter).to receive(:upload_anchor_data).and_return(nil) + + expect(subject).to be false + end + + it 'returns true when upload_anchor_data exists' do + allow(presenter).to receive(:upload_anchor_data).and_return(true) + + expect(subject).to be true + end + end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 7ea238c0607..e64b5ddc374 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -30,7 +30,7 @@ RSpec.describe API::NpmProjectPackages do end describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do - let_it_be(:package_file) { package.package_files.first } + let(:package_file) { package.package_files.first } let(:headers) { {} } let(:url) { api("/projects/#{project.id}/packages/npm/#{package.name}/-/#{package_file.file_name}") } @@ -127,24 +127,6 @@ RSpec.describe API::NpmProjectPackages do context 'when params are correct' do context 'invalid package record' do - context 'unscoped package' do - let(:package_name) { 'my_unscoped_package' } - let(:params) { upload_params(package_name: package_name) } - - it_behaves_like 'handling invalid record with 400 error' - - context 'with empty versions' do - let(:params) { upload_params(package_name: package_name).merge!(versions: {}) } - - it 'throws a 400 error' do - expect { upload_package_with_token(package_name, params) } - .not_to change { project.packages.count } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - context 'invalid package name' do let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } let(:params) { upload_params(package_name: package_name) } @@ -175,52 +157,71 @@ RSpec.describe API::NpmProjectPackages do end end - context 'scoped package' do - let(:package_name) { "@#{group.path}/my_package_name" } + context 'valid package record' do let(:params) { upload_params(package_name: package_name) } - context 'with access token' do - subject { upload_package_with_token(package_name, params) } + shared_examples 'handling upload with different authentications' do + context 'with access token' do + subject { upload_package_with_token(package_name, params) } + + it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package' + + it 'creates npm package with file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Tag.count }.by(1) - it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package' + expect(response).to have_gitlab_http_status(:ok) + end + end - it 'creates npm package with file' do - expect { subject } + it 'creates npm package with file with job token' do + expect { upload_package_with_job_token(package_name, params) } .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) - .and change { Packages::Tag.count }.by(1) expect(response).to have_gitlab_http_status(:ok) end - end - it 'creates npm package with file with job token' do - expect { upload_package_with_job_token(package_name, params) } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) + context 'with an authenticated job token' do + let!(:job) { create(:ci_build, user: user) } - expect(response).to have_gitlab_http_status(:ok) - end + before do + Grape::Endpoint.before_each do |endpoint| + expect(endpoint).to receive(:current_authenticated_job) { job } + end + end - context 'with an authenticated job token' do - let!(:job) { create(:ci_build, user: user) } + after do + Grape::Endpoint.before_each nil + end - before do - Grape::Endpoint.before_each do |endpoint| - expect(endpoint).to receive(:current_authenticated_job) { job } + it 'creates the package metadata' do + upload_package_with_token(package_name, params) + + expect(response).to have_gitlab_http_status(:ok) + expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline end end + end - after do - Grape::Endpoint.before_each nil - end + context 'with a scoped name' do + let(:package_name) { "@#{group.path}/my_package_name" } - it 'creates the package metadata' do - upload_package_with_token(package_name, params) + it_behaves_like 'handling upload with different authentications' + end - expect(response).to have_gitlab_http_status(:ok) - expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline - end + context 'with any scoped name' do + let(:package_name) { "@any_scope/my_package_name" } + + it_behaves_like 'handling upload with different authentications' + end + + context 'with an unscoped name' do + let(:package_name) { "my_unscoped_package_name" } + + it_behaves_like 'handling upload with different authentications' end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 611f12c8146..80de453cd8b 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -166,20 +166,6 @@ RSpec.describe MergeRequests::MergeService do service.execute(merge_request) end - context 'when jira_issue_transition_id is not present' do - before do - allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) - end - - it 'does not close issue' do - jira_tracker.update!(jira_issue_transition_id: nil) - - expect_any_instance_of(JiraService).not_to receive(:transition_issue) - - service.execute(merge_request) - end - end - context 'wrong issue markdown' do it 'does not close issues on Jira issue tracker' do jira_issue = ExternalIssue.new('#JIRA-123', project) diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index 10fce6c1651..ba5729eaf59 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Packages::Npm::CreatePackageService do end let(:override) { {} } - let(:package_name) { "@#{namespace.path}/my-app".freeze } + let(:package_name) { "@#{namespace.path}/my-app" } subject { described_class.new(project, user, params).execute } @@ -42,29 +42,35 @@ RSpec.describe Packages::Npm::CreatePackageService do it { expect(subject.name).to eq(package_name) } it { expect(subject.version).to eq(version) } + + context 'with build info' do + let(:job) { create(:ci_build, user: user) } + let(:params) { super().merge(build: job) } + + it_behaves_like 'assigns build to package' + it_behaves_like 'assigns status to package' + + it 'creates a package file build info' do + expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) + end + end end describe '#execute' do context 'scoped package' do it_behaves_like 'valid package' + end - context 'with build info' do - let(:job) { create(:ci_build, user: user) } - let(:params) { super().merge(build: job) } - - it_behaves_like 'assigns build to package' - it_behaves_like 'assigns status to package' + context 'scoped package not following the naming convention' do + let(:package_name) { '@any-scope/package' } - it 'creates a package file build info' do - expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) - end - end + it_behaves_like 'valid package' end - context 'invalid package name' do - let(:package_name) { "@#{namespace.path}/my-group/my-app".freeze } + context 'unscoped package' do + let(:package_name) { 'unscoped-package' } - it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid) } + it_behaves_like 'valid package' end context 'package already exists' do @@ -84,11 +90,18 @@ RSpec.describe Packages::Npm::CreatePackageService do it { expect(subject[:message]).to be 'File is too large.' } end - context 'with incorrect namespace' do - let(:package_name) { '@my_other_namespace/my-app' } - - it 'raises a RecordInvalid error' do - expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + [ + '@inv@lid_scope/package', + '@scope/sub/group', + '@scope/../../package', + '@scope%2e%2e%2fpackage' + ].each do |invalid_package_name| + context "with invalid name #{invalid_package_name}" do + let(:package_name) { invalid_package_name } + + it 'raises a RecordInvalid error' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + end end end diff --git a/spec/support/shared_contexts/project_service_jira_context.rb b/spec/support/shared_contexts/project_service_jira_context.rb index 8e01de70846..54bb9fd108e 100644 --- a/spec/support/shared_contexts/project_service_jira_context.rb +++ b/spec/support/shared_contexts/project_service_jira_context.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.shared_context 'project service Jira context' do - let(:url) { 'http://jira.example.com' } - let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } + let(:url) { 'https://jira.example.com' } + let(:test_url) { 'https://jira.example.com/rest/api/2/serverInfo' } def fill_form(disable: false) click_active_checkbox if disable @@ -10,6 +10,5 @@ RSpec.shared_context 'project service Jira context' do fill_in 'service_url', with: url fill_in 'service_username', with: 'username' fill_in 'service_password', with: 'password' - fill_in 'service_jira_issue_transition_id', with: '25' end end diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb index b4b9ab456e0..a8e75c624e8 100644 --- a/spec/support/shared_contexts/project_service_shared_context.rb +++ b/spec/support/shared_contexts/project_service_shared_context.rb @@ -15,7 +15,10 @@ RSpec.shared_context 'project service activation' do def visit_project_integration(name) visit_project_integrations - click_link(name) + + within('#content-body') do + click_link(name) + end end def click_active_checkbox diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb index 60a29d78084..b0f57b1969f 100644 --- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb @@ -22,6 +22,10 @@ RSpec.shared_context 'set package name from package name type' do case package_name_type when :scoped_naming_convention "@#{group.path}/scoped-package" + when :scoped_no_naming_convention + '@any-scope/scoped-package' + when :unscoped + 'unscoped-package' when :non_existing 'non-existing-package' end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 25203fa3182..4411c91d479 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -3,7 +3,13 @@ RSpec.shared_examples 'it uploads and commit a new text file' do it 'uploads and commit a new text file', :js do find('.add-to-tree').click - click_link('Upload file') + + page.within('.dropdown-menu') do + click_link('Upload file') + + wait_for_requests + end + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) page.within('#modal-upload-blob') do @@ -29,7 +35,13 @@ end RSpec.shared_examples 'it uploads and commit a new image file' do it 'uploads and commit a new image file', :js do find('.add-to-tree').click - click_link('Upload file') + + page.within('.dropdown-menu') do + click_link('Upload file') + + wait_for_requests + end + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')) page.within('#modal-upload-blob') do diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index be051dcbb7b..eb7f6c7a723 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -46,94 +46,178 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do - nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok - nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok - nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected - nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found - nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found - nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found - nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected - nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found - nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found - nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found - nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected - nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found - - :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found - :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found - :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden - :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found - :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok - :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok - :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok - :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected - :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected - :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found - :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found - - :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found - :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found - :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden - :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found - :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok - :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok - :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected - :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected - :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found - :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found - - :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok - :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found - :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok - :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found - :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok - :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok - :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected - :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found - - :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found - :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found - :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok - :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok - :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected - :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found + nil | :scoped_naming_convention | true | :public | nil | :accept | :ok + nil | :scoped_naming_convention | false | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | true | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | false | :public | nil | :accept | :ok + nil | :unscoped | true | :public | nil | :accept | :ok + nil | :unscoped | false | :public | nil | :accept | :ok + nil | :non_existing | true | :public | nil | :redirect | :redirected + nil | :non_existing | false | :public | nil | :reject | :not_found + nil | :scoped_naming_convention | true | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | false | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | true | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | false | :private | nil | :reject | :not_found + nil | :unscoped | true | :private | nil | :reject | :not_found + nil | :unscoped | false | :private | nil | :reject | :not_found + nil | :non_existing | true | :private | nil | :redirect | :redirected + nil | :non_existing | false | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | true | :internal | nil | :reject | :not_found + nil | :scoped_naming_convention | false | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | true | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | false | :internal | nil | :reject | :not_found + nil | :unscoped | true | :internal | nil | :reject | :not_found + nil | :unscoped | false | :internal | nil | :reject | :not_found + nil | :non_existing | true | :internal | nil | :redirect | :redirected + nil | :non_existing | false | :internal | nil | :reject | :not_found + + :oauth | :scoped_naming_convention | true | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | true | :public | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | false | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok + :oauth | :unscoped | true | :public | :guest | :accept | :ok + :oauth | :unscoped | true | :public | :reporter | :accept | :ok + :oauth | :unscoped | false | :public | :guest | :accept | :ok + :oauth | :unscoped | false | :public | :reporter | :accept | :ok + :oauth | :non_existing | true | :public | :guest | :redirect | :redirected + :oauth | :non_existing | true | :public | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :public | :guest | :reject | :not_found + :oauth | :non_existing | false | :public | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | true | :private | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | false | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok + :oauth | :unscoped | true | :private | :guest | :reject | :forbidden + :oauth | :unscoped | true | :private | :reporter | :accept | :ok + :oauth | :unscoped | false | :private | :guest | :reject | :forbidden + :oauth | :unscoped | false | :private | :reporter | :accept | :ok + :oauth | :non_existing | true | :private | :guest | :redirect | :redirected + :oauth | :non_existing | true | :private | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :private | :guest | :reject | :forbidden + :oauth | :non_existing | false | :private | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | true | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok + :oauth | :scoped_naming_convention | false | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok + :oauth | :unscoped | true | :internal | :guest | :accept | :ok + :oauth | :unscoped | true | :internal | :reporter | :accept | :ok + :oauth | :unscoped | false | :internal | :guest | :accept | :ok + :oauth | :unscoped | false | :internal | :reporter | :accept | :ok + :oauth | :non_existing | true | :internal | :guest | :redirect | :redirected + :oauth | :non_existing | true | :internal | :reporter | :redirect | :redirected + :oauth | :non_existing | false | :internal | :guest | :reject | :not_found + :oauth | :non_existing | false | :internal | :reporter | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | true | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | true | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | true | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | false | :public | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :public | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :public | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :public | :guest | :reject | :not_found + :personal_access_token | :non_existing | false | :public | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | true | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | false | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | true | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | false | :private | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :private | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :private | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | false | :private | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | true | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | true | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | true | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | false | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | false | :internal | :reporter | :accept | :ok + :personal_access_token | :non_existing | true | :internal | :guest | :redirect | :redirected + :personal_access_token | :non_existing | true | :internal | :reporter | :redirect | :redirected + :personal_access_token | :non_existing | false | :internal | :guest | :reject | :not_found + :personal_access_token | :non_existing | false | :internal | :reporter | :reject | :not_found + + :job_token | :scoped_naming_convention | true | :public | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :public | :developer | :accept | :ok + :job_token | :unscoped | true | :public | :developer | :accept | :ok + :job_token | :unscoped | false | :public | :developer | :accept | :ok + :job_token | :non_existing | true | :public | :developer | :redirect | :redirected + :job_token | :non_existing | false | :public | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | true | :private | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :private | :developer | :accept | :ok + :job_token | :unscoped | true | :private | :developer | :accept | :ok + :job_token | :unscoped | false | :private | :developer | :accept | :ok + :job_token | :non_existing | true | :private | :developer | :redirect | :redirected + :job_token | :non_existing | false | :private | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | true | :internal | :developer | :accept | :ok + :job_token | :scoped_naming_convention | false | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | true | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | false | :internal | :developer | :accept | :ok + :job_token | :unscoped | true | :internal | :developer | :accept | :ok + :job_token | :unscoped | false | :internal | :developer | :accept | :ok + :job_token | :non_existing | true | :internal | :developer | :redirect | :redirected + :job_token | :non_existing | false | :internal | :developer | :reject | :not_found + + :deploy_token | :scoped_naming_convention | true | :public | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :public | nil | :accept | :ok + :deploy_token | :unscoped | true | :public | nil | :accept | :ok + :deploy_token | :unscoped | false | :public | nil | :accept | :ok + :deploy_token | :non_existing | true | :public | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | true | :private | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :private | nil | :accept | :ok + :deploy_token | :unscoped | true | :private | nil | :accept | :ok + :deploy_token | :unscoped | false | :private | nil | :accept | :ok + :deploy_token | :non_existing | true | :private | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | true | :internal | nil | :accept | :ok + :deploy_token | :scoped_naming_convention | false | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | true | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | false | :internal | nil | :accept | :ok + :deploy_token | :unscoped | true | :internal | nil | :accept | :ok + :deploy_token | :unscoped | false | :internal | nil | :accept | :ok + :deploy_token | :non_existing | true | :internal | nil | :redirect | :redirected + :deploy_token | :non_existing | false | :internal | nil | :reject | :not_found end with_them do @@ -156,7 +240,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| before do project.send("add_#{user_role}", user) if user_role - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) package.update!(name: package_name) unless package_name == 'non-existing-package' stub_application_setting(npm_package_requests_forwarding: request_forward) end @@ -225,26 +309,44 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok - :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok - :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found - :non_existing | 'PUBLIC' | :guest | :reject | :not_found - :non_existing | 'PUBLIC' | :reporter | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok - :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found - :non_existing | 'PRIVATE' | :guest | :reject | :forbidden - :non_existing | 'PRIVATE' | :reporter | :reject | :not_found - - :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok - :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found - :non_existing | 'INTERNAL' | :guest | :reject | :not_found - :non_existing | 'INTERNAL' | :reporter | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :accept | :ok + :scoped_naming_convention | :public | :guest | :accept | :ok + :scoped_naming_convention | :public | :reporter | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :accept | :ok + :scoped_no_naming_convention | :public | :guest | :accept | :ok + :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :unscoped | :public | :anonymous | :accept | :ok + :unscoped | :public | :guest | :accept | :ok + :unscoped | :public | :reporter | :accept | :ok + :non_existing | :public | :anonymous | :reject | :not_found + :non_existing | :public | :guest | :reject | :not_found + :non_existing | :public | :reporter | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :reporter | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :reporter | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :reporter | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :not_found + :scoped_naming_convention | :internal | :guest | :accept | :ok + :scoped_naming_convention | :internal | :reporter | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :not_found + :unscoped | :internal | :guest | :accept | :ok + :unscoped | :internal | :reporter | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :not_found + :non_existing | :internal | :guest | :reject | :not_found + :non_existing | :internal | :reporter | :reject | :not_found end with_them do @@ -254,7 +356,7 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} package tags request" @@ -303,26 +405,44 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden - :non_existing | 'PUBLIC' | :guest | :reject | :forbidden - :non_existing | 'PUBLIC' | :developer | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok - :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found - :non_existing | 'PRIVATE' | :guest | :reject | :forbidden - :non_existing | 'PRIVATE' | :developer | :reject | :not_found - - :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden - :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden - :non_existing | 'INTERNAL' | :guest | :reject | :forbidden - :non_existing | 'INTERNAL' | :developer | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_naming_convention | :public | :guest | :reject | :forbidden + :scoped_naming_convention | :public | :developer | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :scoped_no_naming_convention | :public | :developer | :accept | :ok + :unscoped | :public | :anonymous | :reject | :forbidden + :unscoped | :public | :guest | :reject | :forbidden + :unscoped | :public | :developer | :accept | :ok + :non_existing | :public | :anonymous | :reject | :forbidden + :non_existing | :public | :guest | :reject | :forbidden + :non_existing | :public | :developer | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :developer | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :developer | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :developer | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :developer | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_naming_convention | :internal | :developer | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_no_naming_convention | :internal | :developer | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :forbidden + :unscoped | :internal | :guest | :reject | :forbidden + :unscoped | :internal | :developer | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :forbidden + :non_existing | :internal | :guest | :reject | :forbidden + :non_existing | :internal | :developer | :reject | :not_found end with_them do @@ -332,7 +452,7 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} create package tag request" @@ -379,19 +499,44 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| shared_examples 'handling different package names, visibilities and user roles' do where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do - :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok - :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden - :non_existing | 'PUBLIC' | :guest | :reject | :forbidden - :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found - - :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found - :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden - :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok - :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden - :non_existing | 'INTERNAL' | :guest | :reject | :forbidden - :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found + :scoped_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_naming_convention | :public | :guest | :reject | :forbidden + :scoped_naming_convention | :public | :maintainer | :accept | :ok + :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :scoped_no_naming_convention | :public | :maintainer | :accept | :ok + :unscoped | :public | :anonymous | :reject | :forbidden + :unscoped | :public | :guest | :reject | :forbidden + :unscoped | :public | :maintainer | :accept | :ok + :non_existing | :public | :anonymous | :reject | :forbidden + :non_existing | :public | :guest | :reject | :forbidden + :non_existing | :public | :maintainer | :reject | :not_found + + :scoped_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_naming_convention | :private | :guest | :reject | :forbidden + :scoped_naming_convention | :private | :maintainer | :accept | :ok + :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found + :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :scoped_no_naming_convention | :private | :maintainer | :accept | :ok + :unscoped | :private | :anonymous | :reject | :not_found + :unscoped | :private | :guest | :reject | :forbidden + :unscoped | :private | :maintainer | :accept | :ok + :non_existing | :private | :anonymous | :reject | :not_found + :non_existing | :private | :guest | :reject | :forbidden + :non_existing | :private | :maintainer | :reject | :not_found + + :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_naming_convention | :internal | :maintainer | :accept | :ok + :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden + :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok + :unscoped | :internal | :anonymous | :reject | :forbidden + :unscoped | :internal | :guest | :reject | :forbidden + :unscoped | :internal | :maintainer | :accept | :ok + :non_existing | :internal | :anonymous | :reject | :forbidden + :non_existing | :internal | :guest | :reject | :forbidden + :non_existing | :internal | :maintainer | :reject | :not_found end with_them do @@ -401,7 +546,7 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| before do project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false)) + project.update!(visibility: visibility.to_s) end example_name = "#{params[:expected_result]} delete package tag request" diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index aaae0988602..be501318920 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -93,6 +93,29 @@ RSpec.describe PostReceive do perform end + + it 'tracks an event for the empty_repo_upload experiment', :snowplow do + allow_next_instance_of(ApplicationExperiment) do |e| + allow(e).to receive(:should_track?).and_return(true) + allow(e).to receive(:track_initial_writes) + end + + perform + + expect_snowplow_event(category: 'empty_repo_upload', action: 'initial_write', context: [{ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', data: anything }]) + end + + it 'does not track an event for the empty_repo_upload experiment when project is not empty', :snowplow do + allow(empty_project).to receive(:empty_repo?).and_return(false) + allow_next_instance_of(ApplicationExperiment) do |e| + allow(e).to receive(:should_track?).and_return(true) + allow(e).to receive(:track_initial_writes) + end + + perform + + expect_no_snowplow_event + end end shared_examples 'not updating remote mirrors' do @@ -159,7 +182,7 @@ RSpec.describe PostReceive do end it 'expires the status cache' do - expect(project.repository).to receive(:empty?).and_return(true) + expect(project.repository).to receive(:empty?).at_least(:once).and_return(true) expect(project.repository).to receive(:expire_status_cache) perform diff --git a/vendor/project_templates/dotnetcore.tar.gz b/vendor/project_templates/dotnetcore.tar.gz Binary files differindex 402153aa88d..7e4406dbb23 100644 --- a/vendor/project_templates/dotnetcore.tar.gz +++ b/vendor/project_templates/dotnetcore.tar.gz diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz Binary files differindex f9a52fde41b..bd746dbfb5b 100644 --- a/vendor/project_templates/express.tar.gz +++ b/vendor/project_templates/express.tar.gz |