Commit 7f1f4bfb authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 4ab0d072 4eceeb13
...@@ -41,16 +41,6 @@ Graphql/Descriptions: ...@@ -41,16 +41,6 @@ Graphql/Descriptions:
- 'ee/app/graphql/types/vulnerability_severity_enum.rb' - 'ee/app/graphql/types/vulnerability_severity_enum.rb'
- 'ee/app/graphql/types/vulnerability_state_enum.rb' - 'ee/app/graphql/types/vulnerability_state_enum.rb'
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/267606
FactoryBot/InlineAssociation:
Exclude:
- 'spec/factories/atlassian_identities.rb'
- 'spec/factories/events.rb'
- 'spec/factories/git_wiki_commit_details.rb'
- 'spec/factories/gitaly/commit.rb'
- 'spec/factories/group_group_links.rb'
- 'spec/factories/import_export_uploads.rb'
# WIP: See https://gitlab.com/gitlab-org/gitlab/-/issues/220040 # WIP: See https://gitlab.com/gitlab-org/gitlab/-/issues/220040
Rails/SaveBang: Rails/SaveBang:
Exclude: Exclude:
......
<script> <script>
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarEpicSelect from 'ee_component/boards/components/sidebar/board_sidebar_epic_select.vue';
import BoardSidebarWeightInput from 'ee_component/boards/components/sidebar/board_sidebar_weight_input.vue';
import SidebarIterationWidget from 'ee_component/sidebar/components/sidebar_iteration_widget.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue'; import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
...@@ -26,9 +23,12 @@ export default { ...@@ -26,9 +23,12 @@ export default {
BoardSidebarDueDate, BoardSidebarDueDate,
BoardSidebarSubscription, BoardSidebarSubscription,
BoardSidebarMilestoneSelect, BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect, BoardSidebarEpicSelect: () =>
SidebarIterationWidget, import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
BoardSidebarWeightInput, BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarIterationWidget: () =>
import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
......
...@@ -11,10 +11,12 @@ import { ...@@ -11,10 +11,12 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { partition, isString } from 'lodash'; import { partition, isString } from 'lodash';
import Api from '~/api'; import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import GroupSelect from '~/invite_members/components/group_select.vue'; import GroupSelect from '~/invite_members/components/group_select.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { INVITE_MEMBERS_IN_COMMENT } from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
...@@ -122,8 +124,9 @@ export default { ...@@ -122,8 +124,9 @@ export default {
usersToAddById.map((user) => user.id).join(','), usersToAddById.map((user) => user.id).join(','),
]; ];
}, },
openModal({ inviteeType }) { openModal({ inviteeType, source }) {
this.inviteeType = inviteeType; this.inviteeType = inviteeType;
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId); this.$root.$emit(BV_SHOW_MODAL, this.modalId);
}, },
...@@ -138,6 +141,12 @@ export default { ...@@ -138,6 +141,12 @@ export default {
} }
this.closeModal(); this.closeModal();
}, },
trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
const tracking = new ExperimentTracking(INVITE_MEMBERS_IN_COMMENT);
tracking.event('comment_invite_success');
}
},
cancelInvite() { cancelInvite() {
this.selectedAccessLevel = this.defaultAccessLevel; this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined; this.selectedDate = undefined;
...@@ -177,6 +186,8 @@ export default { ...@@ -177,6 +186,8 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById))); promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
} }
this.trackInvite();
Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError); Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError);
}, },
inviteByEmailPostData(usersToInviteByEmail) { inviteByEmailPostData(usersToInviteByEmail) {
......
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -26,10 +27,29 @@ export default { ...@@ -26,10 +27,29 @@ export default {
required: false, required: false,
default: undefined, default: undefined,
}, },
triggerSource: {
type: String,
required: false,
default: 'unknown',
},
trackExperiment: {
type: String,
required: false,
default: undefined,
},
},
mounted() {
this.trackExperimentOnShow();
}, },
methods: { methods: {
openModal() { openModal() {
eventHub.$emit('openModal', { inviteeType: 'members' }); eventHub.$emit('openModal', { inviteeType: 'members', source: this.triggerSource });
},
trackExperimentOnShow() {
if (this.trackExperiment) {
const tracking = new ExperimentTracking(this.trackExperiment);
tracking.event('comment_invite_shown');
}
}, },
}, },
}; };
......
export const SEARCH_DELAY = 200; export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
...@@ -3,6 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -3,6 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { IssuableType } from '~/issuable_show/constants'; import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue'; import Issue from '~/issue';
import '~/notes/index'; import '~/notes/index';
...@@ -34,6 +35,7 @@ export default function initShowIssue() { ...@@ -34,6 +35,7 @@ export default function initShowIssue() {
initIssueHeaderActions(store); initIssueHeaderActions(store);
initSentryErrorStackTraceApp(); initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp(); initRelatedMergeRequestsApp();
initInviteMembersModal();
import(/* webpackChunkName: 'design_management' */ '~/design_management') import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default()) .then((module) => module.default())
......
...@@ -5,6 +5,7 @@ import initPipelines from '~/commit/pipelines/pipelines_bundle'; ...@@ -5,6 +5,7 @@ import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { handleLocationHash } from '~/lib/utils/common_utils'; import { handleLocationHash } from '~/lib/utils/common_utils';
import StatusBox from '~/merge_request/components/status_box.vue'; import StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph'; import initSourcegraph from '~/sourcegraph';
...@@ -20,6 +21,7 @@ export default function initMergeRequestShow() { ...@@ -20,6 +21,7 @@ export default function initMergeRequestShow() {
loadAwardsHandler(); loadAwardsHandler();
initInviteMemberModal(); initInviteMemberModal();
initInviteMemberTrigger(); initInviteMemberTrigger();
initInviteMembersModal();
const el = document.querySelector('.js-mr-status-box'); const el = document.querySelector('.js-mr-status-box');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
......
<script> <script>
import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
import { isExperimentVariant } from '~/experimentation/utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
export default { export default {
inviteMembersInComment: INVITE_MEMBERS_IN_COMMENT,
components: { components: {
GlButton, GlButton,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlSprintf, GlSprintf,
GlIcon, GlIcon,
InviteMembersTrigger,
}, },
props: { props: {
markdownDocsPath: { markdownDocsPath: {
...@@ -29,6 +34,9 @@ export default { ...@@ -29,6 +34,9 @@ export default {
hasQuickActionsDocsPath() { hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== ''; return this.quickActionsDocsPath !== '';
}, },
inviteCommentEnabled() {
return isExperimentVariant(INVITE_MEMBERS_IN_COMMENT, 'invite_member_link');
},
}, },
}; };
</script> </script>
...@@ -37,9 +45,9 @@ export default { ...@@ -37,9 +45,9 @@ export default {
<div class="comment-toolbar clearfix"> <div class="comment-toolbar clearfix">
<div class="toolbar-text"> <div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
<gl-link :href="markdownDocsPath" target="_blank">{{ <gl-link :href="markdownDocsPath" target="_blank">
__('Markdown is supported') {{ __('Markdown is supported') }}
}}</gl-link> </gl-link>
</template> </template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath"> <template v-if="hasQuickActionsDocsPath && markdownDocsPath">
<gl-sprintf <gl-sprintf
...@@ -59,6 +67,16 @@ export default { ...@@ -59,6 +67,16 @@ export default {
</template> </template>
</div> </div>
<span v-if="canAttachFile" class="uploading-container"> <span v-if="canAttachFile" class="uploading-container">
<invite-members-trigger
v-if="inviteCommentEnabled"
classes="gl-mr-3 gl-vertical-align-text-bottom"
:display-text="s__('InviteMember|Invite Member')"
icon="assignee"
variant="link"
:track-experiment="$options.inviteMembersInComment"
:trigger-source="$options.inviteMembersInComment"
data-track-event="comment_invite_click"
/>
<span class="uploading-progress-container hide"> <span class="uploading-progress-container hide">
<gl-icon name="media" /> <gl-icon name="media" />
<span class="attaching-file-message"></span> <span class="attaching-file-message"></span>
......
...@@ -55,6 +55,15 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -55,6 +55,15 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
experiment_instance.use {}
experiment_instance.try(:invite_member_link) {}
experiment_instance.track(:view, property: @project.root_ancestor.id.to_s)
end
end end
around_action :allow_gitaly_ref_name_caching, only: [:discussions] around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
...@@ -45,6 +45,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -45,6 +45,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
experiment_instance.use {}
experiment_instance.try(:invite_member_link) {}
experiment_instance.track(:view, property: @project.root_ancestor.id.to_s)
end
end end
before_action do before_action do
......
...@@ -1848,7 +1848,7 @@ class Project < ApplicationRecord ...@@ -1848,7 +1848,7 @@ class Project < ApplicationRecord
# where().update_all to perform update in the single transaction with check for null # where().update_all to perform update in the single transaction with check for null
ProjectPagesMetadatum ProjectPagesMetadatum
.where(project_id: id, pages_deployment_id: nil) .where(project_id: id, pages_deployment_id: nil)
.update_all(pages_deployment_id: deployment.id) .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
end end
def write_repository_config(gl_full_path: full_path) def write_repository_config(gl_full_path: full_path)
......
...@@ -33,8 +33,12 @@ module Ci ...@@ -33,8 +33,12 @@ module Ci
end end
def runner_variables def runner_variables
if Feature.enabled?(:variable_inside_variable, project)
variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
else
variables.to_runner_variables variables.to_runner_variables
end end
end
def refspecs def refspecs
specs = [] specs = []
......
...@@ -10,7 +10,11 @@ module Ci ...@@ -10,7 +10,11 @@ module Ci
Result = Struct.new(:build, :build_json, :valid?) Result = Struct.new(:build, :build_json, :valid?)
MAX_QUEUE_DEPTH = 50 ##
# The queue depth limit number has been determined by observing 95
# percentile of effective queue depth on gitlab.com. This is only likely to
# affect 5% of the worst case scenarios.
MAX_QUEUE_DEPTH = 45
def initialize(runner) def initialize(runner)
@runner = runner @runner = runner
...@@ -105,7 +109,7 @@ module Ci ...@@ -105,7 +109,7 @@ module Ci
builds = builds.queued_before(params[:job_age].seconds.ago) builds = builds.queued_before(params[:job_age].seconds.ago)
end end
if Feature.enabled?(:ci_register_job_service_one_by_one, runner) if Feature.enabled?(:ci_register_job_service_one_by_one, runner, default_enabled: true)
build_ids = builds.pluck(:id) build_ids = builds.pluck(:id)
@metrics.observe_queue_size(-> { build_ids.size }) @metrics.observe_queue_size(-> { build_ids.size })
...@@ -171,7 +175,7 @@ module Ci ...@@ -171,7 +175,7 @@ module Ci
def max_queue_depth def max_queue_depth
@max_queue_depth ||= begin @max_queue_depth ||= begin
if Feature.enabled?(:gitlab_ci_builds_queue_limit, runner, default_enabled: false) if Feature.enabled?(:gitlab_ci_builds_queue_limit, runner, default_enabled: true)
MAX_QUEUE_DEPTH MAX_QUEUE_DEPTH
else else
::Gitlab::Database::MAX_INT_VALUE ::Gitlab::Database::MAX_INT_VALUE
......
...@@ -64,7 +64,7 @@ module Pages ...@@ -64,7 +64,7 @@ module Pages
end end
if result[:status] == :success if result[:status] == :success
@logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds") @logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds: #{result[:message]}")
@counters_lock.synchronize { @migrated += 1 } @counters_lock.synchronize { @migrated += 1 }
else else
@logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}") @logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}")
......
...@@ -30,16 +30,18 @@ module Pages ...@@ -30,16 +30,18 @@ module Pages
zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
if zip_result[:status] == :error if zip_result[:status] == :error
if !project.pages_metadatum&.reload&.pages_deployment &&
Feature.enabled?(:pages_migration_mark_as_not_deployed, project)
project.mark_pages_as_not_deployed
end
return error("Can't create zip archive: #{zip_result[:message]}") return error("Can't create zip archive: #{zip_result[:message]}")
end end
archive_path = zip_result[:archive_path] archive_path = zip_result[:archive_path]
unless archive_path
project.set_first_pages_deployment!(nil)
return success(
message: "Archive not created. Missing public directory in #{project.pages_path} ? Marked project as not deployed")
end
deployment = nil deployment = nil
File.open(archive_path) do |file| File.open(archive_path) do |file|
deployment = project.pages_deployments.create!( deployment = project.pages_deployments.create!(
......
...@@ -19,6 +19,10 @@ module Pages ...@@ -19,6 +19,10 @@ module Pages
def execute def execute
unless resolve_public_dir unless resolve_public_dir
if Feature.enabled?(:pages_migration_mark_as_not_deployed)
return success
end
return error("Can not find valid public dir in #{@input_dir}") return error("Can not find valid public dir in #{@input_dir}")
end end
......
...@@ -4,3 +4,4 @@ ...@@ -4,3 +4,4 @@
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") - page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
= render 'projects/issuable/show', issuable: @issue = render 'projects/issuable/show', issuable: @issue
= render 'shared/issuable/invite_members_trigger', project: @project
...@@ -108,3 +108,6 @@ ...@@ -108,3 +108,6 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit
#js-review-bar #js-review-bar
= render 'shared/issuable/invite_members_trigger', project: @project
- return unless can_import_members?
.js-invite-members-modal{ data: { id: project.id,
name: project.name,
is_project: 'true',
access_levels: ProjectMember.access_level_roles.to_json,
default_access_level: Gitlab::Access::GUEST,
help_link: help_page_url('user/permissions') } }
---
title: Fix bug in Gollum Tags filter
merge_request: 56638
author:
type: fixed
---
title: Add JavaScript, TypeScript, and React support to the semgrep analyzer.
merge_request: 55257
author:
type: added
---
title: Resolve nested variable values sent to the runner
merge_request: 48627
author:
type: added
...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323177 ...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323177
milestone: '13.10' milestone: '13.10'
type: development type: development
group: group::memory group: group::memory
default_enabled: false default_enabled: true
...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323201 ...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323201
milestone: '13.10' milestone: '13.10'
type: development type: development
group: group::continuous integration group: group::continuous integration
default_enabled: false default_enabled: true
---
name: invite_members_in_comment
introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51400'
rollout_issue_url: 'https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/300'
milestone: '13.10'
type: experiment
group: group::expansion
default_enabled: false
...@@ -28,7 +28,7 @@ There are two places defined variables can be used. On the: ...@@ -28,7 +28,7 @@ There are two places defined variables can be used. On the:
| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | | `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
| `resource_group` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | | `resource_group` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
| `include` | yes | GitLab | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. <br/><br/>Predefined project variables are supported: `GITLAB_FEATURES`, `CI_DEFAULT_BRANCH`, and all variables that start with `CI_PROJECT_` (for example `CI_PROJECT_NAME`). | | `include` | yes | GitLab | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. <br/><br/>Predefined project variables are supported: `GITLAB_FEATURES`, `CI_DEFAULT_BRANCH`, and all variables that start with `CI_PROJECT_` (for example `CI_PROJECT_NAME`). |
| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `variables` | yes | GitLab/Runner | The variable expansion is first made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab, and then any unrecognized or unavailable variables are expanded by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `services:[]:name` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `services:[]:name` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
...@@ -61,6 +61,54 @@ The expanded part needs to be in a form of `$variable`, or `${variable}` or `%va ...@@ -61,6 +61,54 @@ The expanded part needs to be in a form of `$variable`, or `${variable}` or `%va
Each form is handled in the same way, no matter which OS/shell handles the job, Each form is handled in the same way, no matter which OS/shell handles the job,
because the expansion is done in GitLab before any runner gets the job. because the expansion is done in GitLab before any runner gets the job.
#### Nested variable expansion
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48627) in GitLab 13.10.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It can be enabled or disabled for a single project.
> - It's disabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enabling-the-nested-variable-expansion-feature). **(FREE SELF)**
GitLab expands job variable values recursively before sending them to the runner. For example:
```yaml
- BUILD_ROOT_DIR: '${CI_BUILDS_DIR}'
- OUT_PATH: '${BUILD_ROOT_DIR}/out'
- PACKAGE_PATH: '${OUT_PATH}/pkg'
```
If nested variable expansion is:
- **Disabled**: the runner receives `${BUILD_ROOT_DIR}/out/pkg`. This is not a valid path.
- **Enabled**: the runner receives a valid, fully-formed path. For example, if `${CI_BUILDS_DIR}` is `/output`, then `PACKAGE_PATH` would be `/output/out/pkg`.
References to unavailable variables are left intact. In this case, the runner
[attempts to expand the variable value](#gitlab-runner-internal-variable-expansion-mechanism) at runtime.
For example, a variable like `CI_BUILDS_DIR` is known by the runner only at runtime.
##### Enabling the nested variable expansion feature **(FREE SELF)**
This feature comes with the `:variable_inside_variable` feature flag disabled by default.
To enable this feature, ask a GitLab administrator with [Rails console access](../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
following command:
```ruby
# For the instance
Feature.enable(:variable_inside_variable)
# For a single project
Feature.enable(:variable_inside_variable, Project.find(<project id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:variable_inside_variable)
# For a single project
Feature.disable(:variable_inside_variable, Project.find(<project id>))
```
### GitLab Runner internal variable expansion mechanism ### GitLab Runner internal variable expansion mechanism
- Supported: project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and - Supported: project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and
...@@ -70,16 +118,17 @@ because the expansion is done in GitLab before any runner gets the job. ...@@ -70,16 +118,17 @@ because the expansion is done in GitLab before any runner gets the job.
The runner uses Go's `os.Expand()` method for variable expansion. It means that it handles The runner uses Go's `os.Expand()` method for variable expansion. It means that it handles
only variables defined as `$variable` and `${variable}`. What's also important, is that only variables defined as `$variable` and `${variable}`. What's also important, is that
the expansion is done only once, so nested variables may or may not work, depending on the the expansion is done only once, so nested variables may or may not work, depending on the
ordering of variables definitions. ordering of variables definitions, and whether [nested variable expansion](#nested-variable-expansion)
is enabled in GitLab.
### Execution shell environment ### Execution shell environment
This is an expansion that takes place during the `script` execution. This is an expansion phase that takes place during the `script` execution.
How it works depends on the used shell (`bash`, `sh`, `cmd`, PowerShell). For example, if the job's Its behavior depends on the shell used (`bash`, `sh`, `cmd`, PowerShell). For example, if the job's
`script` contains a line `echo $MY_VARIABLE-${MY_VARIABLE_2}`, it should be properly handled `script` contains a line `echo $MY_VARIABLE-${MY_VARIABLE_2}`, it should be properly handled
by bash/sh (leaving empty strings or some values depending whether the variables were by bash/sh (leaving empty strings or some values depending whether the variables were
defined or not), but don't work with Windows' `cmd` or PowerShell, since these shells defined or not), but don't work with Windows' `cmd` or PowerShell, since these shells
are using a different variables syntax. use a different variables syntax.
Supported: Supported:
...@@ -88,10 +137,10 @@ Supported: ...@@ -88,10 +137,10 @@ Supported:
`.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules). `.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules).
- The `script` may also use all variables defined in the lines before. So, for example, if you define - The `script` may also use all variables defined in the lines before. So, for example, if you define
a variable `export MY_VARIABLE="test"`: a variable `export MY_VARIABLE="test"`:
- In `before_script`, it works in the following lines of `before_script` and - In `before_script`, it works in the subsequent lines of `before_script` and
all lines of the related `script`. all lines of the related `script`.
- In `script`, it works in the following lines of `script`. - In `script`, it works in the subsequent lines of `script`.
- In `after_script`, it works in following lines of `after_script`. - In `after_script`, it works in subsequent lines of `after_script`.
In the case of `after_script` scripts, they can: In the case of `after_script` scripts, they can:
...@@ -99,7 +148,7 @@ In the case of `after_script` scripts, they can: ...@@ -99,7 +148,7 @@ In the case of `after_script` scripts, they can:
section. section.
- Not use variables defined in `before_script` and `script`. - Not use variables defined in `before_script` and `script`.
These restrictions are because `after_script` scripts are executed in a These restrictions exist because `after_script` scripts are executed in a
[separated shell context](../yaml/README.md#after_script). [separated shell context](../yaml/README.md#after_script).
## Persisted variables ## Persisted variables
......
...@@ -66,3 +66,29 @@ TypeError: $ is not a function ...@@ -66,3 +66,29 @@ TypeError: $ is not a function
``` ```
**Remedy - Try moving the script into a separate repository and point to it to files in the GitLab repository** **Remedy - Try moving the script into a separate repository and point to it to files in the GitLab repository**
## Using Vue component issues
### When rendering a component that uses GlFilteredSearch and the component or its parent uses Vue Apollo
When trying to render our component GlFilteredSearch, you might get an error in the component's `provide` function:
`cannot read suggestionsListClass of undefined`
Currently, `vue-apollo` tries to [manually call a component's `provide()` in the `beforeCreate` part](https://github.com/vuejs/vue-apollo/blob/35e27ec398d844869e1bbbde73c6068b8aabe78a/packages/vue-apollo/src/mixin.js#L149) of the component lifecycle. This means that when a `provide()` references props, which aren't actually setup until after `created`, it will blow up.
See this [closed MR](https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2019#note_514671251) for more context.
**Remedy - try providing `apolloProvider` to the top-level Vue instance options**
VueApollo will skip manually running `provide()` if it sees that an `apolloProvider` is provided in the `$options`.
```patch
new Vue(
el,
+ apolloProvider: {},
render(h) {
return h(App);
},
);
```
...@@ -257,9 +257,9 @@ To enable/disable an OmniAuth provider: ...@@ -257,9 +257,9 @@ To enable/disable an OmniAuth provider:
1. In the top navigation bar, go to **Admin Area**. 1. In the top navigation bar, go to **Admin Area**.
1. In the left sidebar, go to **Settings**. 1. In the left sidebar, go to **Settings**.
1. Scroll to the **Sign-in Restrictions** section, and click **Expand**. 1. Scroll to the **Sign-in Restrictions** section, and click **Expand**.
1. Next to **Enabled OAuth Sign-In sources**, select the check box for each provider you want to enable or disable. 1. Below **Enabled OAuth Sign-In sources**, select the check box for each provider you want to enable or disable.
![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) ![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources_v13_10.png)
## Disabling OmniAuth ## Disabling OmniAuth
......
...@@ -65,7 +65,7 @@ GitLab SAST supports a variety of languages, package managers, and frameworks. O ...@@ -65,7 +65,7 @@ GitLab SAST supports a variety of languages, package managers, and frameworks. O
You can also [view our language roadmap](https://about.gitlab.com/direction/secure/static-analysis/sast/#language-support) and [request other language support by opening an issue](https://gitlab.com/groups/gitlab-org/-/epics/297). You can also [view our language roadmap](https://about.gitlab.com/direction/secure/static-analysis/sast/#language-support) and [request other language support by opening an issue](https://gitlab.com/groups/gitlab-org/-/epics/297).
| Language (package managers) / framework | Scan tool | Introduced in GitLab Version | | Language (package managers) / framework | Scan tool | Introduced in GitLab Version |
|--------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| |---------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
| .NET Core | [Security Code Scan](https://security-code-scan.github.io) | 11.0 | | .NET Core | [Security Code Scan](https://security-code-scan.github.io) | 11.0 |
| .NET Framework | [Security Code Scan](https://security-code-scan.github.io) | 13.0 | | .NET Framework | [Security Code Scan](https://security-code-scan.github.io) | 13.0 |
| Apex (Salesforce) | [PMD](https://pmd.github.io/pmd/index.html) | 12.1 | | Apex (Salesforce) | [PMD](https://pmd.github.io/pmd/index.html) | 12.1 |
...@@ -77,6 +77,7 @@ You can also [view our language roadmap](https://about.gitlab.com/direction/secu ...@@ -77,6 +77,7 @@ You can also [view our language roadmap](https://about.gitlab.com/direction/secu
| Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) | | Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) |
| Java (Android) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 | | Java (Android) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 |
| JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 | | JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 |
| JavaScript | [Semgrep](https://semgrep.dev) | 13.10 |
| Kotlin (Android) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 | | Kotlin (Android) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 |
| Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 | | Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 |
| Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 | | Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 |
...@@ -85,11 +86,13 @@ You can also [view our language roadmap](https://about.gitlab.com/direction/secu ...@@ -85,11 +86,13 @@ You can also [view our language roadmap](https://about.gitlab.com/direction/secu
| Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 | | Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 |
| Python | [Semgrep](https://semgrep.dev) | 13.9 | | Python | [Semgrep](https://semgrep.dev) | 13.9 |
| React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 | | React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 |
| React | [Semgrep](https://semgrep.dev) | 13.10 |
| Ruby | [brakeman](https://brakemanscanner.org) | 13.9 | | Ruby | [brakeman](https://brakemanscanner.org) | 13.9 |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3 | | Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3 |
| Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) | | Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) |
| Swift (iOS) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 | | Swift (iOS) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 |
| TypeScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.9, [merged](https://gitlab.com/gitlab-org/gitlab/-/issues/36059) with ESLint in 13.2 | | TypeScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.9, [merged](https://gitlab.com/gitlab-org/gitlab/-/issues/36059) with ESLint in 13.2 |
| TypeScript | [Semgrep](https://semgrep.dev) | 13.10 |
Note that the Java analyzers can also be used for variants like the Note that the Java analyzers can also be used for variants like the
[Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html),
......
...@@ -193,17 +193,21 @@ GitLab allows users to create multiple value streams, hide default stages and cr ...@@ -193,17 +193,21 @@ GitLab allows users to create multiple value streams, hide default stages and cr
### Stage path ### Stage path
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](../../../administration/feature_flags.md). **(FREE SELF)**
![Value stream path navigation](img/vsa_path_nav_v13_10.png "Value stream path navigation")
Stages are visually depicted as a horizontal process flow. Selecting a stage will update the Stages are visually depicted as a horizontal process flow. Selecting a stage updates the content below the value stream.
the content below the value stream.
This is disabled by default. If you have a self-managed instance, an This is enabled by default. If you have a self-managed instance, an
administrator can [open a Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md) administrator can [open a Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md)
and enable it with the following command: and disable it with the following command:
```ruby ```ruby
Feature.enable(:value_stream_analytics_path_navigation) Feature.disable(:value_stream_analytics_path_navigation)
``` ```
### Adding a stage ### Adding a stage
......
...@@ -14,7 +14,7 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati ...@@ -14,7 +14,7 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati
before_action do before_action do
push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true) push_frontend_feature_flag(:cycle_analytics_scatterplot_enabled, default_enabled: true)
push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group) push_frontend_feature_flag(:value_stream_analytics_path_navigation, @group, default_enabled: :yaml)
push_frontend_feature_flag(:value_stream_analytics_extended_form, @group, default_enabled: :yaml) push_frontend_feature_flag(:value_stream_analytics_extended_form, @group, default_enabled: :yaml)
render_403 unless can?(current_user, :read_group_cycle_analytics, @group) render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
end end
......
---
title: Display VSA navigation as a horizontal flow
merge_request: 56632
author:
type: added
--- ---
name: value_stream_analytics_path_navigation name: value_stream_analytics_path_navigation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31069 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31069
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323982
milestone: '13.0' milestone: '13.0'
type: development type: development
group: group::optimize group: group::optimize
default_enabled: false default_enabled: true
...@@ -15,12 +15,14 @@ describe('ee/BoardContentSidebar', () => { ...@@ -15,12 +15,14 @@ describe('ee/BoardContentSidebar', () => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
sidebarType: ISSUABLE, sidebarType: ISSUABLE,
issues: { [mockIssue.id]: mockIssue }, issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
activeId: mockIssue.id, activeId: mockIssue.id,
issuableType: issuableTypes.issue, issuableType: issuableTypes.issue,
}, },
getters: { getters: {
activeIssue: () => mockIssue, activeIssue: () => {
return { ...mockIssue, epic: null };
},
projectPathForActiveIssue: () => mockIssueProjectPath, projectPathForActiveIssue: () => mockIssueProjectPath,
groupPathForActiveIssue: () => mockIssueGroupPath, groupPathForActiveIssue: () => mockIssueGroupPath,
isSidebarOpen: () => true, isSidebarOpen: () => true,
...@@ -31,11 +33,18 @@ describe('ee/BoardContentSidebar', () => { ...@@ -31,11 +33,18 @@ describe('ee/BoardContentSidebar', () => {
}; };
const createComponent = () => { const createComponent = () => {
/*
Dynamically imported components (in our case ee imports)
aren't stubbed automatically in VTU v1:
https://github.com/vuejs/vue-test-utils/issues/1279.
This requires us to additionally mock apollo or vuex stores.
*/
wrapper = shallowMount(BoardContentSidebar, { wrapper = shallowMount(BoardContentSidebar, {
provide: { provide: {
canUpdate: true, canUpdate: true,
rootPath: '/', rootPath: '/',
groupId: '#', groupId: 1,
}, },
store, store,
stubs: { stubs: {
...@@ -49,6 +58,12 @@ describe('ee/BoardContentSidebar', () => { ...@@ -49,6 +58,12 @@ describe('ee/BoardContentSidebar', () => {
participants: { participants: {
loading: false, loading: false,
}, },
currentIteration: {
loading: false,
},
iterations: {
loading: false,
},
}, },
}, },
}, },
......
...@@ -6,35 +6,35 @@ pre-push: ...@@ -6,35 +6,35 @@ pre-push:
eslint: eslint:
tags: frontend style tags: frontend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.{js,vue}" glob: '*.{js,vue}'
run: yarn run lint:eslint {files} run: yarn run lint:eslint {files}
haml-lint: haml-lint:
tags: view haml style tags: view haml style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.html.haml" glob: '*.html.haml'
run: bundle exec haml-lint --config .haml-lint.yml {files} run: bundle exec haml-lint --config .haml-lint.yml {files}
markdownlint: markdownlint:
tags: documentation style tags: documentation style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "doc/*.md" glob: 'doc/*.md'
run: yarn markdownlint {files} run: yarn markdownlint {files}
stylelint: stylelint:
tags: stylesheet css style tags: stylesheet css style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.scss{,.css}" glob: '*.scss{,.css}'
run: yarn stylelint -q {files} run: yarn stylelint {files}
prettier: prettier:
tags: frontend style tags: frontend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.{js,vue,graphql}" glob: '*.{js,vue,graphql}'
run: yarn run prettier --check {files} run: yarn run prettier --check {files}
rubocop: rubocop:
tags: backend style tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.rb" glob: '*.rb'
run: bundle exec rubocop --parallel --force-exclusion {files} run: bundle exec rubocop --parallel --force-exclusion {files}
vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters
tags: documentation style tags: documentation style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "doc/*.md" glob: 'doc/*.md'
run: if command -v vale 2> /dev/null; then vale --config .vale.ini --minAlertLevel error {files}; else echo "Vale not found. Install Vale"; fi run: if command -v vale 2> /dev/null; then vale --config .vale.ini --minAlertLevel error {files}; else echo "Vale not found. Install Vale"; fi
...@@ -98,14 +98,15 @@ module Banzai ...@@ -98,14 +98,15 @@ module Banzai
return unless image?(content) return unless image?(content)
path =
if url?(content) if url?(content)
path = content content
elsif file = wiki.find_file(content, load_content: false) elsif file = wiki.find_file(content, load_content: false)
path = ::File.join(wiki_base_path, file.path) file.path
end end
if path if path
content_tag(:img, nil, data: { src: path }, class: 'gfm') content_tag(:img, nil, src: path, class: 'gfm')
end end
end end
......
...@@ -5,7 +5,7 @@ module Banzai ...@@ -5,7 +5,7 @@ module Banzai
class WikiPipeline < FullPipeline class WikiPipeline < FullPipeline
def self.filters def self.filters
@filters ||= begin @filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) super.insert_before(Filter::ImageLazyLoadFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
end end
end end
......
...@@ -303,6 +303,10 @@ semgrep-sast: ...@@ -303,6 +303,10 @@ semgrep-sast:
$SAST_EXPERIMENTAL_FEATURES == 'true' $SAST_EXPERIMENTAL_FEATURES == 'true'
exists: exists:
- '**/*.py' - '**/*.py'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
sobelow-sast: sobelow-sast:
extends: .sast-analyzer extends: .sast-analyzer
......
...@@ -16875,6 +16875,9 @@ msgstr "" ...@@ -16875,6 +16875,9 @@ msgstr ""
msgid "InviteMember|Don't worry, you can always invite teammates later" msgid "InviteMember|Don't worry, you can always invite teammates later"
msgstr "" msgstr ""
msgid "InviteMember|Invite Member"
msgstr ""
msgid "InviteMember|Invite Members (optional)" msgid "InviteMember|Invite Members (optional)"
msgstr "" msgstr ""
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"file-coverage": "scripts/frontend/file_test_coverage.js", "file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh", "lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue", "internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue",
"internal:stylelint": "stylelint -q '{ee/,}app/assets/stylesheets/**/*.{css,scss}'",
"prejest": "yarn check-dependencies", "prejest": "yarn check-dependencies",
"jest": "jest --config jest.config.js", "jest": "jest --config jest.config.js",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
...@@ -32,7 +33,7 @@ ...@@ -32,7 +33,7 @@
"lint:prettier:fix": "yarn run prettier --write '**/*.{graphql,js,vue}'", "lint:prettier:fix": "yarn run prettier --write '**/*.{graphql,js,vue}'",
"lint:prettier:staged": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --check", "lint:prettier:staged": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --check",
"lint:prettier:staged:fix": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --write", "lint:prettier:staged:fix": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --write",
"lint:stylelint": "stylelint -q '{ee/,}app/assets/stylesheets/**/*.{css,scss}'", "lint:stylelint": "stylelint '{ee/,}app/assets/stylesheets/**/*.{css,scss}'",
"lint:stylelint:fix": "yarn run lint:stylelint --fix", "lint:stylelint:fix": "yarn run lint:stylelint --fix",
"lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q", "lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix", "lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
......
...@@ -33,7 +33,7 @@ class StaticAnalysis ...@@ -33,7 +33,7 @@ class StaticAnalysis
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13, %w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13,
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13, (Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13,
%w[bin/rake config_lint] => 11, %w[bin/rake config_lint] => 11,
%w[yarn run lint:stylelint] => 9, %w[yarn run internal:stylelint] => 9,
%w[scripts/lint-conflicts.sh] => 0.59, %w[scripts/lint-conflicts.sh] => 0.59,
%w[yarn run block-dependencies] => 0.35, %w[yarn run block-dependencies] => 0.35,
%w[scripts/lint-rugged] => 0.23, %w[scripts/lint-rugged] => 0.23,
......
...@@ -209,6 +209,32 @@ RSpec.describe Projects::IssuesController do ...@@ -209,6 +209,32 @@ RSpec.describe Projects::IssuesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email }) expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email })
end end
context 'with the invite_members_in_comment experiment', :experiment do
context 'when user can invite' do
before do
stub_experiments(invite_members_in_comment: :invite_member_link)
project.add_maintainer(user)
end
it 'assigns the candidate experience and tracks the event' do
expect(experiment(:invite_member_link)).to track(:view, property: project.root_ancestor.id.to_s)
.on_any_instance
.for(:invite_member_link)
.with_context(namespace: project.root_ancestor)
get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
end
end
context 'when user can not invite' do
it 'does not track the event' do
expect(experiment(:invite_member_link)).not_to track(:view)
get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
end
end
end
end end
describe 'GET #new' do describe 'GET #new' do
......
...@@ -40,6 +40,32 @@ RSpec.describe Projects::MergeRequestsController do ...@@ -40,6 +40,32 @@ RSpec.describe Projects::MergeRequestsController do
get :show, params: params.merge(extra_params) get :show, params: params.merge(extra_params)
end end
context 'with the invite_members_in_comment experiment', :experiment do
context 'when user can invite' do
before do
stub_experiments(invite_members_in_comment: :invite_member_link)
project.add_maintainer(user)
end
it 'assigns the candidate experience and tracks the event' do
expect(experiment(:invite_member_link)).to track(:view, property: project.root_ancestor.id.to_s)
.on_any_instance
.for(:invite_member_link)
.with_context(namespace: project.root_ancestor)
go
end
end
context 'when user can not invite' do
it 'does not track the event' do
expect(experiment(:invite_member_link)).not_to track(:view)
go
end
end
end
context 'with view param' do context 'with view param' do
before do before do
go(view: 'parallel') go(view: 'parallel')
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
FactoryBot.define do FactoryBot.define do
factory :atlassian_identity, class: 'Atlassian::Identity' do factory :atlassian_identity, class: 'Atlassian::Identity' do
extern_uid { generate(:username) } extern_uid { generate(:username) }
user { create(:user) } user { association(:user) }
expires_at { 2.weeks.from_now } expires_at { 2.weeks.from_now }
token { SecureRandom.alphanumeric(1254) } token { SecureRandom.alphanumeric(1254) }
refresh_token { SecureRandom.alphanumeric(45) } refresh_token { SecureRandom.alphanumeric(45) }
......
...@@ -27,17 +27,20 @@ FactoryBot.define do ...@@ -27,17 +27,20 @@ FactoryBot.define do
factory :wiki_page_event do factory :wiki_page_event do
action { :created } action { :created }
# rubocop: disable FactoryBot/InlineAssociation
# A persistent project is needed to have a wiki page being created properly.
project { @overrides[:wiki_page]&.container || create(:project, :wiki_repo) } project { @overrides[:wiki_page]&.container || create(:project, :wiki_repo) }
target { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) } # rubocop: enable FactoryBot/InlineAssociation
target { association(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
transient do transient do
wiki_page { create(:wiki_page, container: project) } wiki_page { association(:wiki_page, container: project) }
end end
end end
trait :has_design do trait :has_design do
transient do transient do
design { create(:design, issue: create(:issue, project: project)) } design { association(:design, issue: association(:issue, project: project)) }
end end
end end
...@@ -45,7 +48,7 @@ FactoryBot.define do ...@@ -45,7 +48,7 @@ FactoryBot.define do
has_design has_design
transient do transient do
note { create(:note, author: author, project: project, noteable: design) } note { association(:note, author: author, project: project, noteable: design) }
end end
action { :commented } action { :commented }
......
...@@ -5,7 +5,7 @@ FactoryBot.define do ...@@ -5,7 +5,7 @@ FactoryBot.define do
skip_create skip_create
transient do transient do
author { create(:user) } author { association(:user) }
end end
sequence(:message) { |n| "Commit message #{n}" } sequence(:message) { |n| "Commit message #{n}" }
......
...@@ -14,7 +14,7 @@ FactoryBot.define do ...@@ -14,7 +14,7 @@ FactoryBot.define do
subject { "My commit" } subject { "My commit" }
body { subject + "\nMy body" } body { subject + "\nMy body" }
author { build(:gitaly_commit_author) } author { association(:gitaly_commit_author) }
committer { build(:gitaly_commit_author) } committer { association(:gitaly_commit_author) }
end end
end end
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
FactoryBot.define do FactoryBot.define do
factory :group_group_link do factory :group_group_link do
shared_group { create(:group) } shared_group { association(:group) }
shared_with_group { create(:group) } shared_with_group { association(:group) }
group_access { Gitlab::Access::DEVELOPER } group_access { Gitlab::Access::DEVELOPER }
trait(:guest) { group_access { Gitlab::Access::GUEST } } trait(:guest) { group_access { Gitlab::Access::GUEST } }
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :import_export_upload do factory :import_export_upload do
project { create(:project) } project { association(:project) }
end end
end end
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "User invites from a comment", :js do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { project.owner }
before do
sign_in(user)
end
it "launches the invite modal from invite link on a comment" do
stub_experiments(invite_members_in_comment: :invite_member_link)
visit project_issue_path(project, issue)
page.within(".new-note") do
click_button 'Invite Member'
end
expect(page).to have_content("You're inviting members to the")
end
end
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "User invites from a comment", :js do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:user) { project.owner }
before do
sign_in(user)
end
it "launches the invite modal from invite link on a comment" do
stub_experiments(invite_members_in_comment: :invite_member_link)
visit project_merge_request_path(project, merge_request)
page.within(".new-note") do
click_button 'Invite Member'
end
expect(page).to have_content("You're inviting members to the")
end
end
...@@ -19,12 +19,14 @@ describe('BoardContentSidebar', () => { ...@@ -19,12 +19,14 @@ describe('BoardContentSidebar', () => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
sidebarType: ISSUABLE, sidebarType: ISSUABLE,
issues: { [mockIssue.id]: mockIssue }, issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
activeId: mockIssue.id, activeId: mockIssue.id,
issuableType: 'issue', issuableType: 'issue',
}, },
getters: { getters: {
activeIssue: () => mockIssue, activeIssue: () => {
return { ...mockIssue, epic: null };
},
groupPathForActiveIssue: () => mockIssueGroupPath, groupPathForActiveIssue: () => mockIssueGroupPath,
projectPathForActiveIssue: () => mockIssueProjectPath, projectPathForActiveIssue: () => mockIssueProjectPath,
isSidebarOpen: () => true, isSidebarOpen: () => true,
...@@ -35,11 +37,18 @@ describe('BoardContentSidebar', () => { ...@@ -35,11 +37,18 @@ describe('BoardContentSidebar', () => {
}; };
const createComponent = () => { const createComponent = () => {
/*
Dynamically imported components (in our case ee imports)
aren't stubbed automatically in VTU v1:
https://github.com/vuejs/vue-test-utils/issues/1279.
This requires us to additionally mock apollo or vuex stores.
*/
wrapper = shallowMount(BoardContentSidebar, { wrapper = shallowMount(BoardContentSidebar, {
provide: { provide: {
canUpdate: true, canUpdate: true,
rootPath: '/', rootPath: '/',
groupId: '#', groupId: 1,
}, },
store, store,
stubs: { stubs: {
...@@ -53,6 +62,12 @@ describe('BoardContentSidebar', () => { ...@@ -53,6 +62,12 @@ describe('BoardContentSidebar', () => {
participants: { participants: {
loading: false, loading: false,
}, },
currentIteration: {
loading: false,
},
iterations: {
loading: false,
},
}, },
}, },
}, },
...@@ -117,7 +132,7 @@ describe('BoardContentSidebar', () => { ...@@ -117,7 +132,7 @@ describe('BoardContentSidebar', () => {
expect(toggleBoardItem).toHaveBeenCalledTimes(1); expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
boardItem: mockIssue, boardItem: { ...mockIssue, epic: null },
sidebarType: ISSUABLE, sidebarType: ISSUABLE,
}); });
}); });
......
...@@ -3,7 +3,11 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
jest.mock('~/experimentation/experiment_tracking');
const id = '1'; const id = '1';
const name = 'test name'; const name = 'test name';
...@@ -303,6 +307,7 @@ describe('InviteMembersModal', () => { ...@@ -303,6 +307,7 @@ describe('InviteMembersModal', () => {
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
jest.spyOn(wrapper.vm, 'trackInvite');
clickInviteButton(); clickInviteButton();
}); });
...@@ -396,5 +401,46 @@ describe('InviteMembersModal', () => { ...@@ -396,5 +401,46 @@ describe('InviteMembersModal', () => {
}); });
}); });
}); });
describe('tracking', () => {
const postData = {
user_id: '1',
access_level: defaultAccessLevel,
expires_at: undefined,
format: 'json',
};
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
});
it('tracks the invite', () => {
wrapper.vm.openModal({ inviteeType: 'members', source: INVITE_MEMBERS_IN_COMMENT });
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('comment_invite_success');
});
it('does not track invite for unknown source', () => {
wrapper.vm.openModal({ inviteeType: 'members', source: 'unknown' });
clickInviteButton();
expect(ExperimentTracking).not.toHaveBeenCalled();
});
it('does not track invite undefined source', () => {
wrapper.vm.openModal({ inviteeType: 'members' });
clickInviteButton();
expect(ExperimentTracking).not.toHaveBeenCalled();
});
});
}); });
}); });
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
jest.mock('~/experimentation/experiment_tracking');
const displayText = 'Invite team members'; const displayText = 'Invite team members';
let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
return shallowMount(InviteMembersTrigger, { wrapper = shallowMount(InviteMembersTrigger, {
propsData: { propsData: {
displayText, displayText,
...props, ...props,
...@@ -14,7 +19,7 @@ const createComponent = (props = {}) => { ...@@ -14,7 +19,7 @@ const createComponent = (props = {}) => {
}; };
describe('InviteMembersTrigger', () => { describe('InviteMembersTrigger', () => {
let wrapper; const findButton = () => wrapper.findComponent(GlButton);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -22,14 +27,52 @@ describe('InviteMembersTrigger', () => { ...@@ -22,14 +27,52 @@ describe('InviteMembersTrigger', () => {
}); });
describe('displayText', () => { describe('displayText', () => {
const findButton = () => wrapper.findComponent(GlButton); it('includes the correct displayText for the button', () => {
createComponent();
expect(findButton().text()).toBe(displayText);
});
});
describe('clicking the link', () => {
let spy;
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); spy = jest.spyOn(eventHub, '$emit');
}); });
it('includes the correct displayText for the button', () => { it('emits openModal from an unknown source', () => {
expect(findButton().text()).toBe(displayText); createComponent();
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', { inviteeType: 'members', source: 'unknown' });
});
it('emits openModal from a named source', () => {
createComponent({ triggerSource: '_trigger_source_' });
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', {
inviteeType: 'members',
source: '_trigger_source_',
});
});
});
describe('tracking', () => {
it('tracks on mounting', () => {
createComponent({ trackExperiment: '_track_experiment_' });
expect(ExperimentTracking).toHaveBeenCalledWith('_track_experiment_');
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('comment_invite_shown');
});
it('does not track on mounting', () => {
createComponent();
expect(ExperimentTracking).not.toHaveBeenCalledWith('_track_experiment_');
}); });
}); });
}); });
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper'; import { nextTick } from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue'; import Header from '~/vue_merge_request_widget/components/mr_widget_header.vue';
describe('MRWidgetHeader', () => { describe('MRWidgetHeader', () => {
let vm; let wrapper;
let Component;
beforeEach(() => { const createComponent = (propsData = {}) => {
Component = Vue.extend(headerComponent); wrapper = shallowMount(Header, {
propsData,
}); });
};
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
gon.relative_url_root = ''; gon.relative_url_root = '';
}); });
const expectDownloadDropdownItems = () => { const expectDownloadDropdownItems = () => {
const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches'); const downloadEmailPatchesEl = wrapper.find('.js-download-email-patches');
const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff'); const downloadPlainDiffEl = wrapper.find('.js-download-plain-diff');
expect(downloadEmailPatchesEl.innerText.trim()).toEqual('Email patches'); expect(downloadEmailPatchesEl.text().trim()).toBe('Email patches');
expect(downloadEmailPatchesEl.querySelector('a').getAttribute('href')).toEqual( expect(downloadEmailPatchesEl.attributes('href')).toBe('/mr/email-patches');
'/mr/email-patches', expect(downloadPlainDiffEl.text().trim()).toBe('Plain diff');
); expect(downloadPlainDiffEl.attributes('href')).toBe('/mr/plainDiffPath');
expect(downloadPlainDiffEl.innerText.trim()).toEqual('Plain diff');
expect(downloadPlainDiffEl.querySelector('a').getAttribute('href')).toEqual(
'/mr/plainDiffPath',
);
}; };
describe('computed', () => { describe('computed', () => {
describe('shouldShowCommitsBehindText', () => { describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => { it('return true when there are divergedCommitsCount', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -42,11 +39,11 @@ describe('MRWidgetHeader', () => { ...@@ -42,11 +39,11 @@ describe('MRWidgetHeader', () => {
}, },
}); });
expect(vm.shouldShowCommitsBehindText).toEqual(true); expect(wrapper.vm.shouldShowCommitsBehindText).toBe(true);
}); });
it('returns false where there are no divergedComits count', () => { it('returns false where there are no divergedComits count', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 0, divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -56,13 +53,13 @@ describe('MRWidgetHeader', () => { ...@@ -56,13 +53,13 @@ describe('MRWidgetHeader', () => {
}, },
}); });
expect(vm.shouldShowCommitsBehindText).toEqual(false); expect(wrapper.vm.shouldShowCommitsBehindText).toBe(false);
}); });
}); });
describe('commitsBehindText', () => { describe('commitsBehindText', () => {
it('returns singular when there is one commit', () => { it('returns singular when there is one commit', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 1, divergedCommitsCount: 1,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -73,13 +70,13 @@ describe('MRWidgetHeader', () => { ...@@ -73,13 +70,13 @@ describe('MRWidgetHeader', () => {
}, },
}); });
expect(vm.commitsBehindText).toEqual( expect(wrapper.vm.commitsBehindText).toBe(
'The source branch is <a href="/foo/bar/master">1 commit behind</a> the target branch', 'The source branch is <a href="/foo/bar/master">1 commit behind</a> the target branch',
); );
}); });
it('returns plural when there is more than one commit', () => { it('returns plural when there is more than one commit', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 2, divergedCommitsCount: 2,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -90,7 +87,7 @@ describe('MRWidgetHeader', () => { ...@@ -90,7 +87,7 @@ describe('MRWidgetHeader', () => {
}, },
}); });
expect(vm.commitsBehindText).toEqual( expect(wrapper.vm.commitsBehindText).toBe(
'The source branch is <a href="/foo/bar/master">2 commits behind</a> the target branch', 'The source branch is <a href="/foo/bar/master">2 commits behind</a> the target branch',
); );
}); });
...@@ -100,7 +97,7 @@ describe('MRWidgetHeader', () => { ...@@ -100,7 +97,7 @@ describe('MRWidgetHeader', () => {
describe('template', () => { describe('template', () => {
describe('common elements', () => { describe('common elements', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -118,17 +115,17 @@ describe('MRWidgetHeader', () => { ...@@ -118,17 +115,17 @@ describe('MRWidgetHeader', () => {
}); });
it('renders source branch link', () => { it('renders source branch link', () => {
expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual( expect(wrapper.find('.js-source-branch').html()).toContain(
'<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
); );
}); });
it('renders clipboard button', () => { it('renders clipboard button', () => {
expect(vm.$el.querySelector('[data-testid="mr-widget-copy-clipboard"]')).not.toEqual(null); expect(wrapper.find('[data-testid="mr-widget-copy-clipboard"]')).not.toBe(null);
}); });
it('renders target branch', () => { it('renders target branch', () => {
expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master'); expect(wrapper.find('.js-target-branch').text().trim()).toBe('master');
}); });
}); });
...@@ -151,71 +148,68 @@ describe('MRWidgetHeader', () => { ...@@ -151,71 +148,68 @@ describe('MRWidgetHeader', () => {
targetProjectFullPath: 'gitlab-org/gitlab-ce', targetProjectFullPath: 'gitlab-org/gitlab-ce',
}; };
afterEach(() => {
vm.$destroy();
});
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { ...mrDefaultOptions }, mr: { ...mrDefaultOptions },
}); });
}); });
it('renders checkout branch button with modal trigger', () => { it('renders checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch'); const button = wrapper.find('.js-check-out-branch');
expect(button.textContent.trim()).toBe('Check out branch'); expect(button.text().trim()).toBe('Check out branch');
}); });
it('renders web ide button', () => { it('renders web ide button', async () => {
const button = vm.$el.querySelector('.js-web-ide'); const button = wrapper.find('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE'); await nextTick();
expect(button.classList.contains('disabled')).toBe(false);
expect(button.getAttribute('href')).toEqual( expect(button.text().trim()).toBe('Open in Web IDE');
expect(button.classes('disabled')).toBe(false);
expect(button.attributes('href')).toBe(
'/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce', '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce',
); );
}); });
it('renders web ide button in disabled state with no href', () => { it('renders web ide button in disabled state with no href', async () => {
const mr = { ...mrDefaultOptions, canPushToSourceBranch: false }; const mr = { ...mrDefaultOptions, canPushToSourceBranch: false };
vm = mountComponent(Component, { mr }); createComponent({ mr });
await nextTick();
const link = vm.$el.querySelector('.js-web-ide'); const link = wrapper.find('.js-web-ide');
expect(link.classList.contains('disabled')).toBe(true); expect(link.attributes('disabled')).toBe('true');
expect(link.getAttribute('href')).toBeNull(); expect(link.attributes('href')).toBeUndefined();
}); });
it('renders web ide button with blank query string if target & source project branch', (done) => { it('renders web ide button with blank query string if target & source project branch', async () => {
vm.mr.targetProjectFullPath = 'root/gitlab-ce'; createComponent({ mr: { ...mrDefaultOptions, targetProjectFullPath: 'root/gitlab-ce' } });
vm.$nextTick(() => { await nextTick();
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE'); const button = wrapper.find('.js-web-ide');
expect(button.getAttribute('href')).toEqual(
expect(button.text().trim()).toBe('Open in Web IDE');
expect(button.attributes('href')).toBe(
'/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=', '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=',
); );
done();
});
}); });
it('renders web ide button with relative URL', (done) => { it('renders web ide button with relative URL', async () => {
gon.relative_url_root = '/gitlab'; gon.relative_url_root = '/gitlab';
vm.mr.iid = 2;
vm.$nextTick(() => { createComponent({ mr: { ...mrDefaultOptions, iid: 2 } });
const button = vm.$el.querySelector('.js-web-ide');
await nextTick();
const button = wrapper.find('.js-web-ide');
expect(button.textContent.trim()).toEqual('Open in Web IDE'); expect(button.text().trim()).toBe('Open in Web IDE');
expect(button.getAttribute('href')).toEqual( expect(button.attributes('href')).toBe(
'/gitlab/-/ide/project/root/gitlab-ce/merge_requests/2?target_project=gitlab-org%2Fgitlab-ce', '/gitlab/-/ide/project/root/gitlab-ce/merge_requests/2?target_project=gitlab-org%2Fgitlab-ce',
); );
done();
});
}); });
it('renders download dropdown with links', () => { it('renders download dropdown with links', () => {
...@@ -225,7 +219,7 @@ describe('MRWidgetHeader', () => { ...@@ -225,7 +219,7 @@ describe('MRWidgetHeader', () => {
describe('with a closed merge request', () => { describe('with a closed merge request', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -243,9 +237,9 @@ describe('MRWidgetHeader', () => { ...@@ -243,9 +237,9 @@ describe('MRWidgetHeader', () => {
}); });
it('does not render checkout branch button with modal trigger', () => { it('does not render checkout branch button with modal trigger', () => {
const button = vm.$el.querySelector('.js-check-out-branch'); const button = wrapper.find('.js-check-out-branch');
expect(button).toEqual(null); expect(button.exists()).toBe(false);
}); });
it('renders download dropdown with links', () => { it('renders download dropdown with links', () => {
...@@ -255,7 +249,7 @@ describe('MRWidgetHeader', () => { ...@@ -255,7 +249,7 @@ describe('MRWidgetHeader', () => {
describe('without diverged commits', () => { describe('without diverged commits', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 0, divergedCommitsCount: 0,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -273,13 +267,13 @@ describe('MRWidgetHeader', () => { ...@@ -273,13 +267,13 @@ describe('MRWidgetHeader', () => {
}); });
it('does not render diverged commits info', () => { it('does not render diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null); expect(wrapper.find('.diverged-commits-count').exists()).toBe(false);
}); });
}); });
describe('with diverged commits', () => { describe('with diverged commits', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
divergedCommitsCount: 12, divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor', sourceBranch: 'mr-widget-refactor',
...@@ -297,17 +291,13 @@ describe('MRWidgetHeader', () => { ...@@ -297,17 +291,13 @@ describe('MRWidgetHeader', () => {
}); });
it('renders diverged commits info', () => { it('renders diverged commits info', () => {
expect(vm.$el.querySelector('.diverged-commits-count').textContent).toEqual( expect(wrapper.find('.diverged-commits-count').text().trim()).toBe(
'The source branch is 12 commits behind the target branch', 'The source branch is 12 commits behind the target branch',
); );
expect(vm.$el.querySelector('.diverged-commits-count a').textContent).toEqual( expect(wrapper.find('.diverged-commits-count a').text().trim()).toBe('12 commits behind');
'12 commits behind', expect(wrapper.find('.diverged-commits-count a').attributes('href')).toBe(
); wrapper.vm.mr.targetBranchPath,
expect(vm.$el.querySelector('.diverged-commits-count a')).toHaveAttr(
'href',
vm.mr.targetBranchPath,
); );
}); });
}); });
......
import Vue from 'vue'; import { mount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper'; import { isExperimentVariant } from '~/experimentation/utils';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
jest.mock('~/experimentation/utils', () => ({ isExperimentVariant: jest.fn() }));
describe('toolbar', () => { describe('toolbar', () => {
let vm; let wrapper;
const Toolbar = Vue.extend(toolbar);
const props = { const createMountedWrapper = (props = {}) => {
markdownDocsPath: '', wrapper = mount(Toolbar, {
propsData: { markdownDocsPath: '', ...props },
stubs: { 'invite-members-trigger': true },
});
}; };
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
isExperimentVariant.mockReset();
}); });
describe('user can attach file', () => { describe('user can attach file', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Toolbar, props); createMountedWrapper();
}); });
it('should render uploading-container', () => { it('should render uploading-container', () => {
expect(vm.$el.querySelector('.uploading-container')).not.toBeNull(); expect(wrapper.vm.$el.querySelector('.uploading-container')).not.toBeNull();
}); });
}); });
describe('user cannot attach file', () => { describe('user cannot attach file', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Toolbar, { ...props, canAttachFile: false }); createMountedWrapper({ canAttachFile: false });
}); });
it('should not render uploading-container', () => { it('should not render uploading-container', () => {
expect(vm.$el.querySelector('.uploading-container')).toBeNull(); expect(wrapper.vm.$el.querySelector('.uploading-container')).toBeNull();
});
});
describe('user can invite member', () => {
const findInviteLink = () => wrapper.find(InviteMembersTrigger);
beforeEach(() => {
isExperimentVariant.mockReturnValue(true);
createMountedWrapper();
});
it('should render the invite members trigger', () => {
expect(findInviteLink().exists()).toBe(true);
});
it('should have correct props', () => {
expect(findInviteLink().props().displayText).toBe('Invite Member');
expect(findInviteLink().props().trackExperiment).toBe(INVITE_MEMBERS_IN_COMMENT);
expect(findInviteLink().props().triggerSource).toBe(INVITE_MEMBERS_IN_COMMENT);
});
});
describe('user can not invite member', () => {
const findInviteLink = () => wrapper.find(InviteMembersTrigger);
beforeEach(() => {
isExperimentVariant.mockReturnValue(false);
createMountedWrapper();
});
it('should render the invite members trigger', () => {
expect(findInviteLink().exists()).toBe(false);
}); });
}); });
}); });
...@@ -166,7 +166,7 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do ...@@ -166,7 +166,7 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do
end end
context "when the link doesn't exist" do context "when the link doesn't exist" do
let(:mutation_arguments) { super().merge(id: 'gid://gitlab/Releases::Link/999999') } let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
......
...@@ -27,7 +27,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do ...@@ -27,7 +27,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do
tag = '[[images/image.jpg]]' tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", wiki: wiki) doc = filter("See #{tag}", wiki: wiki)
expect(doc.at_css('img')['data-src']).to eq "#{wiki.wiki_base_path}/images/image.jpg" expect(doc.at_css('img')['src']).to eq 'images/image.jpg'
end end
it 'does not creates img tag if image does not exist' do it 'does not creates img tag if image does not exist' do
...@@ -45,7 +45,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do ...@@ -45,7 +45,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do
tag = '[[http://example.com/image.jpg]]' tag = '[[http://example.com/image.jpg]]'
doc = filter("See #{tag}", wiki: wiki) doc = filter("See #{tag}", wiki: wiki)
expect(doc.at_css('img')['data-src']).to eq "http://example.com/image.jpg" expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
end end
it 'does not creates img tag for invalid URL' do it 'does not creates img tag for invalid URL' do
......
...@@ -289,4 +289,29 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do ...@@ -289,4 +289,29 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio%20file%20name.wav"') expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio%20file%20name.wav"')
end end
end end
describe 'gollum tag filters' do
context 'when local image file exists' do
it 'sets the proper attributes for the image' do
gollum_file_double = double('Gollum::File',
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
markdown = "[[#{wiki_file.path}]]"
expect(wiki).to receive(:find_file).with(wiki_file.path, load_content: false).and_return(wiki_file)
output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug)
doc = Nokogiri::HTML::DocumentFragment.parse(output)
full_path = "/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/#{wiki_file.path}"
expect(doc.css('a')[0].attr('href')).to eq(full_path)
expect(doc.css('img')[0].attr('class')).to eq('gfm lazy')
expect(doc.css('img')[0].attr('data-src')).to eq(full_path)
end
end
end
end end
...@@ -821,45 +821,6 @@ RSpec.describe Ci::Build do ...@@ -821,45 +821,6 @@ RSpec.describe Ci::Build do
{ cache: [{ key: "key", paths: ["public"], policy: "pull-push" }] } { cache: [{ key: "key", paths: ["public"], policy: "pull-push" }] }
end end
context 'with multiple_cache_per_job FF disabled' do
before do
stub_feature_flags(multiple_cache_per_job: false)
end
let(:options) { { cache: { key: "key", paths: ["public"], policy: "pull-push" } } }
subject { build.cache }
context 'when build has cache' do
before do
allow(build).to receive(:options).and_return(options)
end
context 'when project has jobs_cache_index' do
before do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
end
it { is_expected.to be_an(Array).and all(include(key: "key-1")) }
end
context 'when project does not have jobs_cache_index' do
before do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(nil)
end
it { is_expected.to eq([options[:cache]]) }
end
end
context 'when build does not have cache' do
before do
allow(build).to receive(:options).and_return({})
end
it { is_expected.to eq([]) }
end
end
subject { build.cache } subject { build.cache }
context 'when build has cache' do context 'when build has cache' do
......
...@@ -6016,12 +6016,15 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6016,12 +6016,15 @@ RSpec.describe Project, factory_default: :keep do
project.set_first_pages_deployment!(deployment) project.set_first_pages_deployment!(deployment)
expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment) expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment)
expect(project.pages_metadatum.reload.deployed).to eq(true)
end end
it "updates the existing metadara record with deployment" do it "updates the existing metadara record with deployment" do
expect do expect do
project.set_first_pages_deployment!(deployment) project.set_first_pages_deployment!(deployment)
end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment) end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
expect(project.pages_metadatum.reload.deployed).to eq(true)
end end
it 'only updates metadata for this project' do it 'only updates metadata for this project' do
...@@ -6030,6 +6033,8 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6030,6 +6033,8 @@ RSpec.describe Project, factory_default: :keep do
expect do expect do
project.set_first_pages_deployment!(deployment) project.set_first_pages_deployment!(deployment)
end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil) end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil)
expect(other_project.pages_metadatum.reload.deployed).to eq(false)
end end
it 'does nothing if metadata already references some deployment' do it 'does nothing if metadata already references some deployment' do
...@@ -6040,6 +6045,14 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6040,6 +6045,14 @@ RSpec.describe Project, factory_default: :keep do
project.set_first_pages_deployment!(deployment) project.set_first_pages_deployment!(deployment)
end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment) end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment)
end end
it 'marks project as not deployed if deployment is nil' do
project.mark_pages_as_deployed
expect do
project.set_first_pages_deployment!(nil)
end.to change { project.pages_metadatum.reload.deployed }.from(true).to(false)
end
end end
describe '#has_pool_repsitory?' do describe '#has_pool_repsitory?' do
......
...@@ -85,7 +85,7 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -85,7 +85,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
Ci::JobArtifact::DEFAULT_FILE_NAMES.each do |file_type, filename| Ci::JobArtifact::DEFAULT_FILE_NAMES.each do |file_type, filename|
context file_type.to_s do context file_type.to_s do
let(:report) { { "#{file_type}": [filename] } } let(:report) { { "#{file_type}": [filename] } }
let(:build) { create(:ci_build, options: { artifacts: { reports: report } } ) } let(:build) { create(:ci_build, options: { artifacts: { reports: report } }) }
let(:report_expectation) do let(:report_expectation) do
{ {
...@@ -106,7 +106,7 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -106,7 +106,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
context "when option has both archive and reports specification" do context "when option has both archive and reports specification" do
let(:report) { { junit: ['junit.xml'] } } let(:report) { { junit: ['junit.xml'] } }
let(:build) { create(:ci_build, options: { script: 'echo', artifacts: { **archive, reports: report } } ) } let(:build) { create(:ci_build, options: { script: 'echo', artifacts: { **archive, reports: report } }) }
let(:report_expectation) do let(:report_expectation) do
{ {
...@@ -272,21 +272,17 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -272,21 +272,17 @@ RSpec.describe Ci::BuildRunnerPresenter do
end end
end end
describe '#variables' do
subject { presenter.variables }
let(:build) { create(:ci_build) }
it 'returns a Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
end
describe '#runner_variables' do describe '#runner_variables' do
subject { presenter.runner_variables } subject { presenter.runner_variables }
let(:build) { create(:ci_build) } let_it_be(:project_with_flag_disabled) { create(:project, :repository) }
let_it_be(:project_with_flag_enabled) { create(:project, :repository) }
before do
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
shared_examples 'returns an array with the expected variables' do
it 'returns an array' do it 'returns an array' do
is_expected.to be_an_instance_of(Array) is_expected.to be_an_instance_of(Array)
end end
...@@ -295,4 +291,63 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -295,4 +291,63 @@ RSpec.describe Ci::BuildRunnerPresenter do
is_expected.to eq(presenter.variables.to_runner_variables) is_expected.to eq(presenter.variables.to_runner_variables)
end end
end end
context 'when FF :variable_inside_variable is disabled' do
let(:sha) { project_with_flag_disabled.repository.commit.sha }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_disabled) }
let(:build) { create(:ci_build, pipeline: pipeline) }
it_behaves_like 'returns an array with the expected variables'
end
context 'when FF :variable_inside_variable is enabled' do
let(:sha) { project_with_flag_enabled.repository.commit.sha }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_enabled) }
let(:build) { create(:ci_build, pipeline: pipeline) }
it_behaves_like 'returns an array with the expected variables'
end
end
describe '#runner_variables subset' do
subject { presenter.runner_variables.select { |v| %w[A B C].include?(v.fetch(:key)) } }
let(:build) { create(:ci_build) }
context 'with references in pipeline variables' do
before do
create(:ci_pipeline_variable, key: 'A', value: 'refA-$B', pipeline: build.pipeline)
create(:ci_pipeline_variable, key: 'B', value: 'refB-$C-$D', pipeline: build.pipeline)
create(:ci_pipeline_variable, key: 'C', value: 'value', pipeline: build.pipeline)
end
context 'when FF :variable_inside_variable is disabled' do
before do
stub_feature_flags(variable_inside_variable: false)
end
it 'returns non-expanded variables' do
is_expected.to eq [
{ key: 'A', value: 'refA-$B', public: false, masked: false },
{ key: 'B', value: 'refB-$C-$D', public: false, masked: false },
{ key: 'C', value: 'value', public: false, masked: false }
]
end
end
context 'when FF :variable_inside_variable is enabled' do
before do
stub_feature_flags(variable_inside_variable: [build.project])
end
it 'returns expanded and sorted variables' do
is_expected.to eq [
{ key: 'C', value: 'value', public: false, masked: false },
{ key: 'B', value: 'refB-value-$D', public: false, masked: false },
{ key: 'A', value: 'refA-refB-value-$D', public: false, masked: false }
]
end
end
end
end
end end
...@@ -48,12 +48,12 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do ...@@ -48,12 +48,12 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do
end end
context 'when pages directory does not exist' do context 'when pages directory does not exist' do
it 'tries to migrate the project, but does not crash' do it 'counts project as migrated' do
expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service| expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original expect(service).to receive(:execute).and_call_original
end end
expect(service.execute).to eq(migrated: 0, errored: 1) expect(service.execute).to eq(migrated: 1, errored: 0)
end end
end end
......
...@@ -11,7 +11,7 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -11,7 +11,7 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(zip_service).to receive(:execute).and_call_original expect(zip_service).to receive(:execute).and_call_original
end end
expect(described_class.new(project, ignore_invalid_entries: true).execute[:status]).to eq(:error) expect(described_class.new(project, ignore_invalid_entries: true).execute[:status]).to eq(:success)
end end
it 'marks pages as not deployed if public directory is absent' do it 'marks pages as not deployed if public directory is absent' do
...@@ -20,8 +20,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -20,8 +20,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
expect(service.execute).to( expect(service.execute).to(
eq(status: :error, eq(status: :success,
message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}") message: "Archive not created. Missing public directory in #{project.pages_path} ? Marked project as not deployed")
) )
expect(project.pages_metadatum.reload.deployed).to eq(false) expect(project.pages_metadatum.reload.deployed).to eq(false)
...@@ -35,8 +35,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -35,8 +35,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
expect(service.execute).to( expect(service.execute).to(
eq(status: :error, eq(status: :success,
message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}") message: "Archive not created. Missing public directory in #{project.pages_path} ? Marked project as not deployed")
) )
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
......
...@@ -12,8 +12,10 @@ RSpec.describe Pages::ZipDirectoryService do ...@@ -12,8 +12,10 @@ RSpec.describe Pages::ZipDirectoryService do
let(:ignore_invalid_entries) { false } let(:ignore_invalid_entries) { false }
let(:service_directory) { @work_dir }
let(:service) do let(:service) do
described_class.new(@work_dir, ignore_invalid_entries: ignore_invalid_entries) described_class.new(service_directory, ignore_invalid_entries: ignore_invalid_entries)
end end
let(:result) do let(:result) do
...@@ -25,32 +27,41 @@ RSpec.describe Pages::ZipDirectoryService do ...@@ -25,32 +27,41 @@ RSpec.describe Pages::ZipDirectoryService do
let(:archive) { result[:archive_path] } let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] } let(:entries_count) { result[:entries_count] }
it 'returns error if project pages dir does not exist' do shared_examples 'handles invalid public directory' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception) it 'returns success' do
expect(status).to eq(:success)
expect( expect(archive).to be_nil
described_class.new("/tmp/not/existing/dir").execute expect(entries_count).to be_nil
).to eq(status: :error, message: "Can not find valid public dir in /tmp/not/existing/dir")
end end
it 'returns nils if there is no public directory and does not leave archive' do it 'returns error if pages_migration_mark_as_not_deployed is disabled' do
stub_feature_flags(pages_migration_mark_as_not_deployed: false)
expect(status).to eq(:error) expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{@work_dir}") expect(message).to eq("Can not find valid public dir in #{service_directory}")
expect(archive).to eq(nil) expect(archive).to be_nil
expect(entries_count).to eq(nil) expect(entries_count).to be_nil
end
end
context "when work direcotry doesn't exist" do
let(:service_directory) { "/tmp/not/existing/dir" }
expect(File.exist?(File.join(@work_dir, '@migrated.zip'))).to eq(false) include_examples 'handles invalid public directory'
end end
it 'returns nils if public directory is a symlink' do context 'when public directory is absent' do
include_examples 'handles invalid public directory'
end
context 'when public directory is a symlink' do
before do
create_dir('target') create_dir('target')
create_file('./target/index.html', 'hello') create_file('./target/index.html', 'hello')
create_link("public", "./target") create_link("public", "./target")
end
expect(status).to eq(:error) include_examples 'handles invalid public directory'
expect(message).to eq("Can not find valid public dir in #{@work_dir}")
expect(archive).to eq(nil)
expect(entries_count).to eq(nil)
end end
context 'when there is a public directory' do context 'when there is a public directory' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment