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:
- 'ee/app/graphql/types/vulnerability_severity_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
Rails/SaveBang:
Exclude:
......
<script>
import { GlDrawer } from '@gitlab/ui';
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 BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
......@@ -26,9 +23,12 @@ export default {
BoardSidebarDueDate,
BoardSidebarSubscription,
BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect,
SidebarIterationWidget,
BoardSidebarWeightInput,
BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarIterationWidget: () =>
import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
},
mixins: [glFeatureFlagsMixin()],
computed: {
......
......@@ -11,10 +11,12 @@ import {
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import GroupSelect from '~/invite_members/components/group_select.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
import { INVITE_MEMBERS_IN_COMMENT } from '../constants';
import eventHub from '../event_hub';
export default {
......@@ -122,8 +124,9 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
openModal({ inviteeType }) {
openModal({ inviteeType, source }) {
this.inviteeType = inviteeType;
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
......@@ -138,6 +141,12 @@ export default {
}
this.closeModal();
},
trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
const tracking = new ExperimentTracking(INVITE_MEMBERS_IN_COMMENT);
tracking.event('comment_invite_success');
}
},
cancelInvite() {
this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined;
......@@ -177,6 +186,8 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
this.trackInvite();
Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError);
},
inviteByEmailPostData(usersToInviteByEmail) {
......
<script>
import { GlButton } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
......@@ -26,10 +27,29 @@ export default {
required: false,
default: undefined,
},
triggerSource: {
type: String,
required: false,
default: 'unknown',
},
trackExperiment: {
type: String,
required: false,
default: undefined,
},
},
mounted() {
this.trackExperimentOnShow();
},
methods: {
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 INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
......@@ -3,6 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
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 Issue from '~/issue';
import '~/notes/index';
......@@ -34,6 +35,7 @@ export default function initShowIssue() {
initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
initInviteMembersModal();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
......
......@@ -5,6 +5,7 @@ import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
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 StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph';
......@@ -20,6 +21,7 @@ export default function initMergeRequestShow() {
loadAwardsHandler();
initInviteMemberModal();
initInviteMemberTrigger();
initInviteMembersModal();
const el = document.querySelector('.js-mr-status-box');
// eslint-disable-next-line no-new
......
<script>
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 {
inviteMembersInComment: INVITE_MEMBERS_IN_COMMENT,
components: {
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
GlIcon,
InviteMembersTrigger,
},
props: {
markdownDocsPath: {
......@@ -29,6 +34,9 @@ export default {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
inviteCommentEnabled() {
return isExperimentVariant(INVITE_MEMBERS_IN_COMMENT, 'invite_member_link');
},
},
};
</script>
......@@ -37,9 +45,9 @@ export default {
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
<gl-link :href="markdownDocsPath" target="_blank">{{
__('Markdown is supported')
}}</gl-link>
<gl-link :href="markdownDocsPath" target="_blank">
{{ __('Markdown is supported') }}
</gl-link>
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
<gl-sprintf
......@@ -59,6 +67,16 @@ export default {
</template>
</div>
<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">
<gl-icon name="media" />
<span class="attaching-file-message"></span>
......
......@@ -55,6 +55,15 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
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
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
......@@ -45,6 +45,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
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
before_action do
......
......@@ -1848,7 +1848,7 @@ class Project < ApplicationRecord
# where().update_all to perform update in the single transaction with check for null
ProjectPagesMetadatum
.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
def write_repository_config(gl_full_path: full_path)
......
......@@ -33,7 +33,11 @@ module Ci
end
def runner_variables
variables.to_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
end
end
def refspecs
......
......@@ -10,7 +10,11 @@ module Ci
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)
@runner = runner
......@@ -105,7 +109,7 @@ module Ci
builds = builds.queued_before(params[:job_age].seconds.ago)
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)
@metrics.observe_queue_size(-> { build_ids.size })
......@@ -171,7 +175,7 @@ module Ci
def max_queue_depth
@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
else
::Gitlab::Database::MAX_INT_VALUE
......
......@@ -64,7 +64,7 @@ module Pages
end
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 }
else
@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
zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
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]}")
end
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
File.open(archive_path) do |file|
deployment = project.pages_deployments.create!(
......
......@@ -19,6 +19,10 @@ module Pages
def execute
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}")
end
......
......@@ -4,3 +4,4 @@
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
= render 'projects/issuable/show', issuable: @issue
= render 'shared/issuable/invite_members_trigger', project: @project
......@@ -108,3 +108,6 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit
#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
milestone: '13.10'
type: development
group: group::memory
default_enabled: false
default_enabled: true
......@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323201
milestone: '13.10'
type: development
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:
| `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). |
| `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) |
| `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) |
......@@ -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,
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
- 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.
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
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
This is an expansion 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
This is an expansion phase that takes place during the `script` execution.
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
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
are using a different variables syntax.
use a different variables syntax.
Supported:
......@@ -88,10 +137,10 @@ Supported:
`.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
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`.
- In `script`, it works in the following lines of `script`.
- In `after_script`, it works in following lines of `after_script`.
- In `script`, it works in the subsequent lines of `script`.
- In `after_script`, it works in subsequent lines of `after_script`.
In the case of `after_script` scripts, they can:
......@@ -99,7 +148,7 @@ In the case of `after_script` scripts, they can:
section.
- 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).
## Persisted variables
......
......@@ -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**
## 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:
1. In the top navigation bar, go to **Admin Area**.
1. In the left sidebar, go to **Settings**.
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
......
This diff is collapsed.
......@@ -193,17 +193,21 @@ GitLab allows users to create multiple value streams, hide default stages and cr
### 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
the content below the value stream.
Stages are visually depicted as a horizontal process flow. Selecting a stage updates 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)
and enable it with the following command:
and disable it with the following command:
```ruby
Feature.enable(:value_stream_analytics_path_navigation)
Feature.disable(:value_stream_analytics_path_navigation)
```
### Adding a stage
......
......@@ -14,7 +14,7 @@ class Groups::Analytics::CycleAnalyticsController < Groups::Analytics::Applicati
before_action do
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)
render_403 unless can?(current_user, :read_group_cycle_analytics, @group)
end
......
---
title: Display VSA navigation as a horizontal flow
merge_request: 56632
author:
type: added
---
name: value_stream_analytics_path_navigation
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'
type: development
group: group::optimize
default_enabled: false
default_enabled: true
......@@ -15,12 +15,14 @@ describe('ee/BoardContentSidebar', () => {
store = new Vuex.Store({
state: {
sidebarType: ISSUABLE,
issues: { [mockIssue.id]: mockIssue },
issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
activeId: mockIssue.id,
issuableType: issuableTypes.issue,
},
getters: {
activeIssue: () => mockIssue,
activeIssue: () => {
return { ...mockIssue, epic: null };
},
projectPathForActiveIssue: () => mockIssueProjectPath,
groupPathForActiveIssue: () => mockIssueGroupPath,
isSidebarOpen: () => true,
......@@ -31,11 +33,18 @@ describe('ee/BoardContentSidebar', () => {
};
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, {
provide: {
canUpdate: true,
rootPath: '/',
groupId: '#',
groupId: 1,
},
store,
stubs: {
......@@ -49,6 +58,12 @@ describe('ee/BoardContentSidebar', () => {
participants: {
loading: false,
},
currentIteration: {
loading: false,
},
iterations: {
loading: false,
},
},
},
},
......
......@@ -6,35 +6,35 @@ pre-push:
eslint:
tags: frontend style
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}
haml-lint:
tags: view haml style
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}
markdownlint:
tags: documentation style
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}
stylelint:
tags: stylesheet css style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.scss{,.css}"
run: yarn stylelint -q {files}
glob: '*.scss{,.css}'
run: yarn stylelint {files}
prettier:
tags: frontend style
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}
rubocop:
tags: backend style
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}
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
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
......@@ -98,14 +98,15 @@ module Banzai
return unless image?(content)
if url?(content)
path = content
elsif file = wiki.find_file(content, load_content: false)
path = ::File.join(wiki_base_path, file.path)
end
path =
if url?(content)
content
elsif file = wiki.find_file(content, load_content: false)
file.path
end
if path
content_tag(:img, nil, data: { src: path }, class: 'gfm')
content_tag(:img, nil, src: path, class: 'gfm')
end
end
......
......@@ -5,7 +5,7 @@ module Banzai
class WikiPipeline < FullPipeline
def self.filters
@filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
super.insert_before(Filter::ImageLazyLoadFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
end
end
......
......@@ -303,6 +303,10 @@ semgrep-sast:
$SAST_EXPERIMENTAL_FEATURES == 'true'
exists:
- '**/*.py'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
sobelow-sast:
extends: .sast-analyzer
......
......@@ -16875,6 +16875,9 @@ msgstr ""
msgid "InviteMember|Don't worry, you can always invite teammates later"
msgstr ""
msgid "InviteMember|Invite Member"
msgstr ""
msgid "InviteMember|Invite Members (optional)"
msgstr ""
......
......@@ -12,6 +12,7 @@
"file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh",
"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",
"jest": "jest --config jest.config.js",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
......@@ -32,7 +33,7 @@
"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: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:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
......
......@@ -33,7 +33,7 @@ class StaticAnalysis
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13,
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13,
%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[yarn run block-dependencies] => 0.35,
%w[scripts/lint-rugged] => 0.23,
......
......@@ -209,6 +209,32 @@ RSpec.describe Projects::IssuesController do
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 })
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
describe 'GET #new' do
......
......@@ -40,6 +40,32 @@ RSpec.describe Projects::MergeRequestsController do
get :show, params: params.merge(extra_params)
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
before do
go(view: 'parallel')
......
......@@ -3,7 +3,7 @@
FactoryBot.define do
factory :atlassian_identity, class: 'Atlassian::Identity' do
extern_uid { generate(:username) }
user { create(:user) }
user { association(:user) }
expires_at { 2.weeks.from_now }
token { SecureRandom.alphanumeric(1254) }
refresh_token { SecureRandom.alphanumeric(45) }
......
......@@ -27,17 +27,20 @@ FactoryBot.define do
factory :wiki_page_event do
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) }
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
wiki_page { create(:wiki_page, container: project) }
wiki_page { association(:wiki_page, container: project) }
end
end
trait :has_design do
transient do
design { create(:design, issue: create(:issue, project: project)) }
design { association(:design, issue: association(:issue, project: project)) }
end
end
......@@ -45,7 +48,7 @@ FactoryBot.define do
has_design
transient do
note { create(:note, author: author, project: project, noteable: design) }
note { association(:note, author: author, project: project, noteable: design) }
end
action { :commented }
......
......@@ -5,7 +5,7 @@ FactoryBot.define do
skip_create
transient do
author { create(:user) }
author { association(:user) }
end
sequence(:message) { |n| "Commit message #{n}" }
......
......@@ -14,7 +14,7 @@ FactoryBot.define do
subject { "My commit" }
body { subject + "\nMy body" }
author { build(:gitaly_commit_author) }
committer { build(:gitaly_commit_author) }
author { association(:gitaly_commit_author) }
committer { association(:gitaly_commit_author) }
end
end
......@@ -2,8 +2,8 @@
FactoryBot.define do
factory :group_group_link do
shared_group { create(:group) }
shared_with_group { create(:group) }
shared_group { association(:group) }
shared_with_group { association(:group) }
group_access { Gitlab::Access::DEVELOPER }
trait(:guest) { group_access { Gitlab::Access::GUEST } }
......
......@@ -2,6 +2,6 @@
FactoryBot.define do
factory :import_export_upload do
project { create(:project) }
project { association(:project) }
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', () => {
store = new Vuex.Store({
state: {
sidebarType: ISSUABLE,
issues: { [mockIssue.id]: mockIssue },
issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
activeId: mockIssue.id,
issuableType: 'issue',
},
getters: {
activeIssue: () => mockIssue,
activeIssue: () => {
return { ...mockIssue, epic: null };
},
groupPathForActiveIssue: () => mockIssueGroupPath,
projectPathForActiveIssue: () => mockIssueProjectPath,
isSidebarOpen: () => true,
......@@ -35,11 +37,18 @@ describe('BoardContentSidebar', () => {
};
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, {
provide: {
canUpdate: true,
rootPath: '/',
groupId: '#',
groupId: 1,
},
store,
stubs: {
......@@ -53,6 +62,12 @@ describe('BoardContentSidebar', () => {
participants: {
loading: false,
},
currentIteration: {
loading: false,
},
iterations: {
loading: false,
},
},
},
},
......@@ -117,7 +132,7 @@ describe('BoardContentSidebar', () => {
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
boardItem: mockIssue,
boardItem: { ...mockIssue, epic: null },
sidebarType: ISSUABLE,
});
});
......
......@@ -3,7 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
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 name = 'test name';
......@@ -303,6 +307,7 @@ describe('InviteMembersModal', () => {
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
jest.spyOn(wrapper.vm, 'trackInvite');
clickInviteButton();
});
......@@ -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 { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
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';
let wrapper;
const createComponent = (props = {}) => {
return shallowMount(InviteMembersTrigger, {
wrapper = shallowMount(InviteMembersTrigger, {
propsData: {
displayText,
...props,
......@@ -14,7 +19,7 @@ const createComponent = (props = {}) => {
};
describe('InviteMembersTrigger', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
afterEach(() => {
wrapper.destroy();
......@@ -22,14 +27,52 @@ describe('InviteMembersTrigger', () => {
});
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(() => {
wrapper = createComponent();
spy = jest.spyOn(eventHub, '$emit');
});
it('includes the correct displayText for the button', () => {
expect(findButton().text()).toBe(displayText);
it('emits openModal from an unknown source', () => {
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 mountComponent from 'helpers/vue_mount_component_helper';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
import { mount } from '@vue/test-utils';
import { isExperimentVariant } from '~/experimentation/utils';
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', () => {
let vm;
const Toolbar = Vue.extend(toolbar);
const props = {
markdownDocsPath: '',
let wrapper;
const createMountedWrapper = (props = {}) => {
wrapper = mount(Toolbar, {
propsData: { markdownDocsPath: '', ...props },
stubs: { 'invite-members-trigger': true },
});
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
isExperimentVariant.mockReset();
});
describe('user can attach file', () => {
beforeEach(() => {
vm = mountComponent(Toolbar, props);
createMountedWrapper();
});
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', () => {
beforeEach(() => {
vm = mountComponent(Toolbar, { ...props, canAttachFile: false });
createMountedWrapper({ canAttachFile: false });
});
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
end
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
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
......
......@@ -27,7 +27,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do
tag = '[[images/image.jpg]]'
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
it 'does not creates img tag if image does not exist' do
......@@ -45,7 +45,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do
tag = '[[http://example.com/image.jpg]]'
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
it 'does not creates img tag for invalid URL' 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"')
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
......@@ -821,45 +821,6 @@ RSpec.describe Ci::Build do
{ cache: [{ key: "key", paths: ["public"], policy: "pull-push" }] }
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 }
context 'when build has cache' do
......
......@@ -6016,12 +6016,15 @@ RSpec.describe Project, factory_default: :keep do
project.set_first_pages_deployment!(deployment)
expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment)
expect(project.pages_metadatum.reload.deployed).to eq(true)
end
it "updates the existing metadara record with deployment" do
expect do
project.set_first_pages_deployment!(deployment)
end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment)
expect(project.pages_metadatum.reload.deployed).to eq(true)
end
it 'only updates metadata for this project' do
......@@ -6030,6 +6033,8 @@ RSpec.describe Project, factory_default: :keep do
expect do
project.set_first_pages_deployment!(deployment)
end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil)
expect(other_project.pages_metadatum.reload.deployed).to eq(false)
end
it 'does nothing if metadata already references some deployment' do
......@@ -6040,6 +6045,14 @@ RSpec.describe Project, factory_default: :keep do
project.set_first_pages_deployment!(deployment)
end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment)
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
describe '#has_pool_repsitory?' do
......
......@@ -85,7 +85,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
Ci::JobArtifact::DEFAULT_FILE_NAMES.each do |file_type, filename|
context file_type.to_s do
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
{
......@@ -106,7 +106,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
context "when option has both archive and reports specification" do
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
{
......@@ -272,27 +272,82 @@ RSpec.describe Ci::BuildRunnerPresenter do
end
end
describe '#variables' do
subject { presenter.variables }
describe '#runner_variables' do
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
is_expected.to be_an_instance_of(Array)
end
it 'returns the expected variables' do
is_expected.to eq(presenter.variables.to_runner_variables)
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 'returns a Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
it_behaves_like 'returns an array with the expected variables'
end
end
describe '#runner_variables' do
subject { presenter.runner_variables }
describe '#runner_variables subset' do
subject { presenter.runner_variables.select { |v| %w[A B C].include?(v.fetch(:key)) } }
let(:build) { create(:ci_build) }
it 'returns an array' do
is_expected.to be_an_instance_of(Array)
end
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 the expected variables' do
is_expected.to eq(presenter.variables.to_runner_variables)
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
......@@ -48,12 +48,12 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do
end
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(service).to receive(:execute).and_call_original
end
expect(service.execute).to eq(migrated: 0, errored: 1)
expect(service.execute).to eq(migrated: 1, errored: 0)
end
end
......
......@@ -11,7 +11,7 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(zip_service).to receive(:execute).and_call_original
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
it 'marks pages as not deployed if public directory is absent' do
......@@ -20,8 +20,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true)
expect(service.execute).to(
eq(status: :error,
message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}")
eq(status: :success,
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)
......@@ -35,8 +35,8 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true)
expect(service.execute).to(
eq(status: :error,
message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}")
eq(status: :success,
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)
......
......@@ -12,8 +12,10 @@ RSpec.describe Pages::ZipDirectoryService do
let(:ignore_invalid_entries) { false }
let(:service_directory) { @work_dir }
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
let(:result) do
......@@ -25,32 +27,41 @@ RSpec.describe Pages::ZipDirectoryService do
let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] }
it 'returns error if project pages dir does not exist' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
shared_examples 'handles invalid public directory' do
it 'returns success' do
expect(status).to eq(:success)
expect(archive).to be_nil
expect(entries_count).to be_nil
end
it 'returns error if pages_migration_mark_as_not_deployed is disabled' do
stub_feature_flags(pages_migration_mark_as_not_deployed: false)
expect(
described_class.new("/tmp/not/existing/dir").execute
).to eq(status: :error, message: "Can not find valid public dir in /tmp/not/existing/dir")
expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{service_directory}")
expect(archive).to be_nil
expect(entries_count).to be_nil
end
end
it 'returns nils if there is no public directory and does not leave archive' do
expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{@work_dir}")
expect(archive).to eq(nil)
expect(entries_count).to eq(nil)
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
it 'returns nils if public directory is a symlink' do
create_dir('target')
create_file('./target/index.html', 'hello')
create_link("public", "./target")
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_file('./target/index.html', 'hello')
create_link("public", "./target")
end
expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{@work_dir}")
expect(archive).to eq(nil)
expect(entries_count).to eq(nil)
include_examples 'handles invalid public directory'
end
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