Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-10 21:09:47 +00:00
parent 6fd750c192
commit 7b2f941669
59 changed files with 1236 additions and 620 deletions

View File

@ -576,12 +576,16 @@ rspec:skipped-flaky-tests-report:
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
variables:
SKIPPED_FLAKY_TESTS_REPORT: skipped_flaky_tests_report.txt
before_script:
- mkdir -p rspec_flaky
script:
- cat rspec_flaky/skipped_flaky_tests_*_report.txt >> skipped_flaky_tests_report.txt
- find rspec_flaky/ -type f -name 'skipped_flaky_tests_*_report.txt' -exec cat {} + >> "${SKIPPED_FLAKY_TESTS_REPORT}"
artifacts:
expire_in: 31d
paths:
- skipped_flaky_tests_report.txt
- ${SKIPPED_FLAKY_TESTS_REPORT}
# EE/FOSS: default refs (MRs, default branch, schedules) jobs #
#######################################################

View File

@ -115,6 +115,9 @@
.if-security-pipeline-merge-result: &if-security-pipeline-merge-result
if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH && $CI_PROJECT_NAMESPACE == "gitlab-org/security" && $GITLAB_USER_LOGIN == "gitlab-release-tools-bot"'
.if-skip-flaky-tests-automatically: &if-skip-flaky-tests-automatically
if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
####################
# Changes patterns #
####################
@ -1357,8 +1360,9 @@
rules:
- <<: *if-not-ee
when: never
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
- <<: *if-skip-flaky-tests-automatically
changes: *code-backstage-patterns
- changes: *ci-patterns
#########################
# Static analysis rules #

View File

@ -110,8 +110,8 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
_Items to be considered if candidate experience is to become a permanent part of GitLab_
<!--
Add a list of items raised during MR review or otherwise that may need further thought/condideration
before making it a permanent part of the product.
Add a list of items raised during MR review or otherwise that may need further thought/consideration
before becoming permanent parts of the product.
Example: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70451#note_727246104
-->

View File

@ -1,10 +1,16 @@
<!-- This template is a great use for issues that are feature::additions or technical tasks for larger issues.-->
### Proposal
### Proposal
<!-- Use this section to explain the feature and how it will work. It can be helpful to add technical details, design proposals, and links to related epics or issues. -->
<!-- Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
-->
<!-- Label reminders
Use the following resources to find the appropriate labels:
- https://gitlab.com/gitlab-org/gitlab/-/labels
- https://about.gitlab.com/handbook/product/categories/features/
-->
/label ~"type::feature" ~feature::addition ~"group::" ~"section::" ~"Category:" ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate"

View File

@ -1,4 +1,4 @@
<!-- This issue template can be used as a great starting point for feature requests. The section "Release notes" can be used as a summary of the feature and is also required if you want to have your release post blog MR auto generated using the release post item generator: https://about.gitlab.com/handbook/marketing/blog/release-posts/#release-post-item-generator. The remaining sections are the backbone for every feature in GitLab.
<!-- This issue template can be used as a great starting point for feature requests. The section "Release notes" can be used as a summary of the feature and is also required if you want to have your release post blog MR auto generated using the release post item generator: https://about.gitlab.com/handbook/marketing/blog/release-posts/#release-post-item-generator. The remaining sections are the backbone for every feature in GitLab.
The goal of this template is brevity for quick/smaller iterations. For a more thorough list of considerations for larger features or feature sets, you can leverage the detailed [feature proposal](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md). -->
@ -34,7 +34,7 @@ Personas are described at https://about.gitlab.com/handbook/marketing/product-ma
* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops)
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#eddie-content-editor)
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#eddie-content-editor)
-->
@ -46,15 +46,10 @@ Create tracking issue using the Snowplow event tracking template. See https://gi
-->
/label ~"type::feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Free"/~"GitLab Premium"/~"GitLab Ultimate"
<!--- Use the following resources to find the appropriate labels:
<!-- Label reminders
Use the following resources to find the appropriate labels:
- https://gitlab.com/gitlab-org/gitlab/-/labels
- https://about.gitlab.com/handbook/product/categories/features/
Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
-->
/label ~documentation
/label ~direction
/label ~"type::feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Free"/~"GitLab Premium"/~"GitLab Ultimate" ~documentation ~direction

View File

@ -313,6 +313,10 @@ That's all of the required database changes.
::Gitlab::GitAccessCoolWidget
end
def self.no_repo_message
git_access_class.error_message(:no_repo)
end
# The feature flag follows the format `geo_#{replicable_name}_replication`,
# so here it would be `geo_cool_widget_replication`
def self.replication_enabled_by_default?
@ -351,6 +355,9 @@ That's all of the required database changes.
```
- [ ] Make sure a Geo secondary site can request and download Cool Widgets on the Geo primary site. You may need to make some changes to `Gitlab::GitAccessCoolWidget`. For example, see [this change for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54914/diffs?commit_id=0f2b36f66697b4addbc69bd377ee2818f648dd33).
- [ ] Make sure a Geo secondary site can replicate Cool Widgets where repository does not exist on the Geo primary site. The only way to know about this is to parse the error text. You may need to make some changes to `Gitlab::CoolWidgetReplicator.no_repo_message` to return the proper error message. For example, see [this change for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74133).
- [ ] Generate the feature flag definition file by running the feature flag command and following the command prompts:
```shell

View File

@ -1,15 +1,15 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
import { s__ } from '~/locale';
export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = __(
'"el" parameter is required for createInstance()',
export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__(
'SourceEditor|"el" parameter is required for createInstance()',
);
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Source Editor instance is required to set up an extension.',
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = s__(
'SourceEditor|Source Editor instance is required to set up an extension.',
);
export const EDITOR_READY_EVENT = 'editor-ready';
@ -20,6 +20,10 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
export const EDITOR_EXTENSION_DEFINITION_ERROR = s__(
'SourceEditor|Extension definition should be either a class or a function',
);
//
// EXTENSIONS' CONSTANTS
//

View File

@ -0,0 +1,116 @@
// THIS IS AN EXAMPLE
//
// This file contains a basic documented example of the Source Editor extensions'
// API for your convenience. You can copy/paste it into your own file
// and adjust as you see fit
//
export class MyFancyExtension {
/**
* THE LIFE-CYCLE CALLBACKS
*/
/**
* Is called before the extension gets used by an instance,
* Use `onSetup` to setup Monaco directly:
* actions, keystrokes, update options, etc.
* Is called only once before the extension gets registered
*
* @param { Object } [setupOptions] The setupOptions object
* @param { Object } [instance] The Source Editor instance
*/
// eslint-disable-next-line class-methods-use-this,no-unused-vars
onSetup(setupOptions, instance) {}
/**
* The first thing called after the extension is
* registered and used by an instance.
* Is called every time the extension is applied
*
* @param { Object } [instance] The Source Editor instance
*/
// eslint-disable-next-line class-methods-use-this,no-unused-vars
onUse(instance) {}
/**
* Is called before un-using an extension. Can be used for time-critical
* actions like cleanup, reverting visual changes, and other user-facing
* updates.
*
* @param { Object } [instance] The Source Editor instance
*/
// eslint-disable-next-line class-methods-use-this,no-unused-vars
onBeforeUnuse(instance) {}
/**
* Is called right after an extension is removed from an instance (un-used)
* Can be used for non time-critical tasks like cleanup on the Monaco level
* (removing actions, keystrokes, etc.).
* onUnuse() will be executed during the browser's idle period
* (https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
*
* @param { Object } [instance] The Source Editor instance
*/
// eslint-disable-next-line class-methods-use-this,no-unused-vars
onUnuse(instance) {}
/**
* The public API of the extension: these are the methods that will be exposed
* to the end user
* @returns {Object}
*/
provides() {
return {
basic: () => {
// The most basic method not depending on anything
// Use: instance.basic();
// eslint-disable-next-line @gitlab/require-i18n-strings
return 'Foo Bar';
},
basicWithProp: () => {
// The methods with access to the props of the extension.
// The props can be either hardcoded (for example in `onSetup`), or
// can be dynamically passed as part of `setupOptions` object when
// using the extension.
// Use: instance.use({ definition: MyFancyExtension, setupOptions: { foo: 'bar' }});
return this.foo;
},
basicWithPropsAsList: (prop1, prop2) => {
// Just a simple method with local props
// The props are passed as usually.
// Use: instance.basicWithPropsAsList(prop1, prop2);
// eslint-disable-next-line @gitlab/require-i18n-strings
return `The prop1 is ${prop1}; the prop2 is ${prop2}`;
},
basicWithInstance: (instance) => {
// The method accessing the instance methods: either own or provided
// by previously-registered extensions
// `instance` is always supplied to all methods in provides() as THE LAST
// argument.
// You don't need to explicitly pass instance to this method:
// Use: instance.basicWithInstance();
// eslint-disable-next-line @gitlab/require-i18n-strings
return `We have access to the whole Instance! ${instance.alpha()}`;
},
advancedWithInstanceAndProps: ({ author, book } = {}, firstname, lastname, instance) => {
// Advanced method where
// { author, book } — are the props passed as an object
// prop1, prop2 — are the props passed as simple list
// instance — is automatically supplied, no need to pass it to
// the method explicitly
// Use: instance.advancedWithInstanceAndProps(
// {
// author: 'Franz Kafka',
// book: 'The Transformation'
// },
// 'Franz',
// 'Kafka'
// );
return `
The author is ${author}; the book is ${book}
The author's name is ${firstname}; the last name is ${lastname}
We have access to the whole Instance! For example, 'instance.alpha()': ${instance.alpha()}`;
},
};
}
}

View File

@ -0,0 +1,17 @@
import { EDITOR_EXTENSION_DEFINITION_ERROR } from './constants';
export default class EditorExtension {
constructor({ definition, setupOptions } = {}) {
if (typeof definition !== 'function') {
throw new Error(EDITOR_EXTENSION_DEFINITION_ERROR);
}
this.name = definition.name; // both class- and fn-based extensions have a name
this.setupOptions = setupOptions;
// eslint-disable-next-line new-cap
this.obj = new definition();
}
get api() {
return this.obj.provides();
}
}

View File

@ -84,9 +84,13 @@ export default {
>
</p>
<gl-table :items="trigger.variables" :fields="$options.fields" small bordered>
<gl-table :items="trigger.variables" :fields="$options.fields" small bordered fixed>
<template #cell(key)="{ item }">
<span class="gl-overflow-break-word">{{ item.key }}</span>
</template>
<template #cell(value)="data">
{{ getDisplayValue(data.value) }}
<span class="gl-overflow-break-word">{{ getDisplayValue(data.value) }}</span>
</template>
</gl-table>
</template>

View File

@ -8,7 +8,6 @@ import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
@ -52,7 +51,9 @@ export default {
update(data) {
return data[this.graphqlResource].packages;
},
debounce: LIST_QUERY_DEBOUNCE_TIME,
skip() {
return !this.sort;
},
},
},
computed: {

View File

@ -83,5 +83,4 @@ export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
export const PROJECT_RESOURCE_TYPE = 'project';
export const GROUP_RESOURCE_TYPE = 'group';
export const LIST_QUERY_DEBOUNCE_TIME = 50;
export const GRAPHQL_PAGE_SIZE = 20;

View File

@ -1,10 +1,3 @@
(async function packageApp() {
if (window.gon.features.packageListApollo) {
const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
import packageList from '~/packages_and_registries/package_registry/pages/list';
newPackageList.default();
} else {
const packageList = await import('~/packages/list/packages_list_app_bundle');
packageList.default();
}
})();
packageList();

View File

@ -1,10 +1,3 @@
(async function packageApp() {
if (window.gon.features.packageListApollo) {
const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
import packageList from '~/packages_and_registries/package_registry/pages/list';
newPackageList.default();
} else {
const packageList = await import('~/packages/list/packages_list_app_bundle');
packageList.default();
}
})();
packageList();

View File

@ -18,8 +18,11 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
}
createdAt
user {
id
name
username
webPath
webUrl
email
avatarUrl
status {

View File

@ -1,10 +1,16 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlTooltip, GlSafeHtmlDirective } from '@gitlab/ui';
import {
GlTooltipDirective,
GlButton,
GlSafeHtmlDirective,
GlAvatarLink,
GlAvatarLabeled,
} from '@gitlab/ui';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
@ -17,10 +23,9 @@ export default {
components: {
CiIconBadge,
TimeagoTooltip,
UserAvatarImage,
GlLink,
GlButton,
GlTooltip,
GlAvatarLink,
GlAvatarLabeled,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -94,6 +99,9 @@ export default {
return this.itemName;
},
userId() {
return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
},
},
methods: {
@ -124,24 +132,32 @@ export default {
{{ __('by') }}
<template v-if="user">
<gl-link
v-gl-tooltip
:href="userPath"
:title="user.email"
class="js-user-link commit-committer-link"
<gl-avatar-link
:data-user-id="userId"
:data-username="user.username"
:data-name="user.name"
:href="user.webUrl"
target="_blank"
class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center"
>
<user-avatar-image :img-src="avatarUrl" :img-alt="userAvatarAltText" :size="24" />
{{ user.name }}
</gl-link>
<gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
{{ message }}
</gl-tooltip>
<span
v-if="statusTooltipHTML"
:ref="$options.EMOJI_REF"
v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
:data-testid="message"
></span>
<gl-avatar-labeled
:size="24"
:src="avatarUrl"
:label="user.name"
class="gl-display-none gl-sm-display-inline-flex gl-mx-1"
/>
<strong class="author gl-display-inline gl-sm-display-none!">@{{ user.username }}</strong>
<gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
{{ message }}
</gl-tooltip>
<span
v-if="statusTooltipHTML"
:ref="$options.EMOJI_REF"
v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
class="gl-ml-2"
:data-testid="message"
></span>
</gl-avatar-link>
</template>
</section>

View File

@ -6,10 +6,6 @@ module Groups
feature_category :package_registry
before_action do
push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
end
private
def verify_packages_enabled!

View File

@ -7,10 +7,6 @@ module Projects
feature_category :package_registry
before_action do
push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
end
def show
@package = project.packages.find(params[:id])
end

View File

@ -50,6 +50,7 @@ class Suggestion < ApplicationRecord
next _("Can't apply as the source branch was deleted.") unless noteable.source_branch_exists?
next outdated_reason if outdated?(cached: cached) || !note.active?
next _("This suggestion already matches its content.") unless different_content?
next _("This file was modified for readability, and can't accept suggestions. Edit it directly.") if file_path.end_with? "ipynb"
end
end

View File

@ -176,7 +176,13 @@ module SystemNotes
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author)
Integrations::CreateExternalCrossReferenceWorker.perform_async(
noteable.project_id,
noteable.id,
mentioner.class.name,
mentioner.id,
author.id
)
else
track_cross_reference_action
create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))

View File

@ -2231,6 +2231,15 @@
:weight: 2
:idempotent: true
:tags: []
- :name: integrations_create_external_cross_reference
:worker_name: Integrations::CreateExternalCrossReferenceWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: invalid_gpg_signature_update
:worker_name: InvalidGpgSignatureUpdateWorker
:feature_category: :source_code_management

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Integrations
class CreateExternalCrossReferenceWorker
include ApplicationWorker
data_consistency :delayed
feature_category :integrations
urgency :low
idempotent!
deduplicate :until_executed, including_scheduled: true
loggable_arguments 2
def perform(project_id, external_issue_id, mentionable_type, mentionable_id, author_id)
project = Project.find_by_id(project_id) || return
author = User.find_by_id(author_id) || return
mentionable = find_mentionable(mentionable_type, mentionable_id, project) || return
external_issue = ExternalIssue.new(external_issue_id, project)
project.external_issue_tracker.create_cross_reference_note(
external_issue,
mentionable,
author
)
end
private
def find_mentionable(mentionable_type, mentionable_id, project)
mentionable_class = mentionable_type.safe_constantize
# Passing an invalid mentionable_class is a developer error, so we don't want to retry the job
# but still track the exception on production, and raise it in development.
unless mentionable_class && mentionable_class < Mentionable
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new("Unexpected class '#{mentionable_type}' is not a Mentionable"))
return
end
if mentionable_type == 'Commit'
project.commit(mentionable_id)
else
mentionable_class.find_by_id(mentionable_id)
end
end
end
end

View File

@ -1,8 +0,0 @@
---
name: package_list_apollo
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70598
rollout_issue_url:
milestone: '14.3'
type: development
group: group::package
default_enabled: false

View File

@ -201,6 +201,8 @@
- 1
- - incident_management_pending_escalations_alert_create
- 1
- - integrations_create_external_cross_reference
- 1
- - invalid_gpg_signature_update
- 2
- - irker

View File

@ -0,0 +1,12 @@
- name: "Deprecate `Versions` on base `PackageType`"
announcement_milestone: "14.5" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-11-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
body: | # Do not modify this line, instead modify the lines below.
As part of the work to create a [Package Registry GraphQL API](https://gitlab.com/groups/gitlab-org/-/epics/6318), the Package group deprecated the `Version` type for the basic `PackageType` type and moved it to [`PackageDetailsType`](https://docs.gitlab.com/ee/api/graphql/reference/index.html#packagedetailstype).
In milestone 15.0, we will completely remove `Version` from `PackageType`.
stage: package
tiers: Free
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327453

View File

@ -0,0 +1,11 @@
- name: "Remove the `:dependency_proxy_for_private_groups` feature flag" # The name of the feature to be deprecated
announcement_milestone: "14.5" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-11-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
body: | # Do not modify this line, instead modify the lines below.
We added a feature flag because [GitLab-#11582](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) changed how public groups use the Dependency Proxy. Prior to this change, you could use the Dependency Proxy without authentication. The change requires authentication to use the Dependency Proxy.
In milestone 15.0, we will remove the feature flag entirely. Moving forward, you must authenticate when using the Dependency Proxy.
stage: package
tiers: Free
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276777

View File

@ -55,6 +55,7 @@ exceptions:
- FAQ
- FIFO
- FIPS
- FLAG
- FOSS
- FQDN
- FREE

View File

@ -129,7 +129,7 @@ and we recommend you use:
### Firewall rules
The following table lists basic ports that must be open between the **primary** and **secondary** sites for Geo.
The following table lists basic ports that must be open between the **primary** and **secondary** sites for Geo. To simplify failovers, we recommend opening ports in both directions.
| Source site | Source port | Destination site | Destination port | Protocol |
|-------------|-------------|------------------|------------------|-------------|

View File

@ -5,5 +5,5 @@ remove_date: '2022-02-09'
This document was moved to [another location](internal_api/index.md).
<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
<!-- This redirect file can be deleted after <2022-02-09>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -58,6 +58,8 @@ repository.
Calls are limited to 50 seconds each.
This endpoint is covered in more detail on [its own page](internal_api_allowed.md), due to the scope of what it covers.
```plaintext
POST /internal/allowed
```

View File

@ -0,0 +1,109 @@
---
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
type: reference, api
---
# Internal allowed API
The `internal/allowed` endpoint assesses whether a user has permission to perform
certain operations on the Git repository. It performs multiple checks, such as:
- Ensuring the branch or tag name is acceptable.
- Whether or not the user has the authority to perform that action.
## Endpoint definition
The internal API endpoints are defined under
[`lib/api/internal`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/api/internal),
and the `/allowed` path is in
[`lib/api/internal/base.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/internal/base.rb).
## Use the endpoint
`internal/allowed` is called when you:
- Push to the repository.
- Perform actions on the repository through the GitLab user interface, such as
applying suggestions or using the GitLab IDE.
Gitaly typically calls this endpoint. It is only called internally (by other parts
of the application) rather than by external users of the API.
## Push checks
A key part of the `internal/allowed` flow is the call to
`EE::Gitlab::Checks::PushRuleCheck`, which can perform the following checks:
- `EE::Gitlab::Checks::PushRules::CommitCheck`
- `EE::Gitlab::Checks::PushRules::TagCheck`
- `EE::Gitlab::Checks::PushRules::BranchCheck`
- `EE::Gitlab::Checks::PushRules::FileSizeCheck`
## Recursion
Some of the Gitaly RPCs called by `internal/allowed` then, themselves, make calls
back to `internal/allowed`. These calls are now correlated with the original request.
Gitaly relies on the Rails application for authorization, and maintains no permissions model itself.
These calls show up in the logs differently to the initial requests. {example}
Because this endpoint can be called recursively, slow performance on this endpoint can result in an exponential performance impact. This documentation is in fact adapted from [the investigation into its performance](https://gitlab.com/gitlab-org/gitlab/-/issues/222247).
## Known performance issues
### Refs
The number of [`refs`](https://git-scm.com/book/en/v2/Git-Internals-Git-References)
on the Git repository have a notable effect on the performance of `git` commands
called upon it. Gitaly RPCs are similarly affected. Certain `git` commands scan
through all refs, causing a notable impact on the speed of those commands.
On the `internal/allowed` endpoint, the recursive nature of RPC calls mean the
ref counts have an exponential effect on performance.
#### Environment refs
[Stale environment refs](https://gitlab.com/gitlab-org/gitlab/-/issues/296625)
are a common example of excessive refs causing performance issues. Stale environment
refs can number into the tens of thousands on busy repositories, as they aren't
cleared up automatically.
#### Dangling refs
Dangling refs are created to prevent accidental deletion of objects from object pools.
Large numbers of these refs can exist, which may have potential performance implications.
For existing discussion around this issue, read
[`gitaly#1900`](https://gitlab.com/gitlab-org/gitaly/-/issues/1900). This issue
appears to have less effect than stale environment refs.
### Pool repositories
When a fork is created on GitLab, a central pool repository is created and the forks
are linked to it. This pool repository prevents duplication of data by storing
data common to other forks. However, the pool repository is not cleaned up in the
same manner as the standard repositories, and is more prone to the refs issue.
## Feature flags
### Parallel push checks
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `parallel_push_checks`.
On GitLab.com, by default this feature is not available. To make it available
per project, ask GitLab.com administrator to
[enable the feature flag](../../administration/feature_flags.md) named `parallel_push_checks`.
You should not use this feature for production environments.
This experimental feature flag enables the endpoint to run multiple RPCs simultaneously,
reducing the overall time taken by roughly half. This time savings is achieved through
threading, and has potential side effects at large scale. On GitLab.com, this feature flag
is enabled only for `gitlab-org/gitlab` and `gitlab-com/www-gitlab-com` projects.
Without it, those projects routinely time out requests to the endpoint. When this
feature was deployed to all of GitLab.com, some pushes failed, presumably due to
exhausting resources like database connection pools.
We recommend you enable this feature flag only if you are experiencing timeouts, and
only enable it for that specific project.

View File

@ -19,15 +19,14 @@ You can create an incident manually or automatically.
### Create incidents manually
> [Permission changed](https://gitlab.com/gitlab-org/gitlab/-/issues/336624) from Guest to Reporter in GitLab 14.5.
> - [Moved](https://gitlab.com/gitlab-org/monitor/monitor/-/issues/24) to GitLab Free in 13.3.
> - [Permission changed](https://gitlab.com/gitlab-org/gitlab/-/issues/336624) from Guest to Reporter in GitLab 14.5.
If you have at least Reporter [permissions](../../user/permissions.md),
you can create an incident manually from the Incidents List or the Issues List.
To create an incident from the Incidents List:
> [Moved](https://gitlab.com/gitlab-org/monitor/monitor/-/issues/24) to GitLab Free in 13.3.
1. Navigate to **Monitor > Incidents** and click **Create Incident**.
1. Create a new issue using the `incident` template available when creating it.
1. Create a new issue and assign the `incident` label to it.

View File

@ -311,8 +311,8 @@ Your own runners can still be used even if you reach your limits.
If you're using GitLab SaaS, you can purchase additional CI minutes so your
pipelines aren't blocked after you have used all your CI minutes from your
main quota. You can find pricing for additional CI/CD minutes in the
[GitLab Customers Portal](https://customers.gitlab.com/plans). Additional minutes:
main quota. You can find pricing for additional CI/CD minutes on the
[GitLab Pricing page](https://about.gitlab.com/pricing/). Additional minutes:
- Are only used after the shared quota included in your subscription runs out.
- Roll over month to month.

View File

@ -58,6 +58,14 @@ dramatically slow down GitLab instances. For this reason, they are being removed
Announced: 2021-09-22
### Deprecate `Versions` on base `PackageType`
As part of the work to create a [Package Registry GraphQL API](https://gitlab.com/groups/gitlab-org/-/epics/6318), the Package group deprecated the `Version` type for the basic `PackageType` type and moved it to [`PackageDetailsType`](https://docs.gitlab.com/ee/api/graphql/reference/index.html#packagedetailstype).
In milestone 15.0, we will completely remove `Version` from `PackageType`.
Announced: 2021-11-22
### GitLab Serverless
[GitLab Serverless](https://docs.gitlab.com/ee/user/project/clusters/serverless/) is a feature set to support Knative-based serverless development with automatic deployments and monitoring.
@ -107,6 +115,14 @@ When checking if a runner is `paused`, API users are advised to check the boolea
Announced: 2021-11-22
### Remove the `:dependency_proxy_for_private_groups` feature flag
We added a feature flag because [GitLab-#11582](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) changed how public groups use the Dependency Proxy. Prior to this change, you could use the Dependency Proxy without authentication. The change requires authentication to use the Dependency Proxy.
In milestone 15.0, we will remove the feature flag entirely. Moving forward, you must authenticate when using the Dependency Proxy.
Announced: 2021-11-22
### `AuthenticationType` for `[runners.cache.s3]` must be explicitly assigned
In GitLab 15.0 and later, to access the AWS S3 cache, you must specify the `AuthenticationType` for [`[runners.cache.s3]`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscaches3-section). The `AuthenticationType` must be `IAM` or `credentials`.

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module BulkImports
module Projects
module Pipelines
class ProtectedBranchesPipeline
include NdjsonPipeline
relation_name 'protected_branches'
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
end
end
end
end

View File

@ -39,6 +39,10 @@ module BulkImports
pipeline: BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline,
stage: 4
},
protected_branches: {
pipeline: BulkImports::Projects::Pipelines::ProtectedBranchesPipeline,
stage: 4
},
wiki: {
pipeline: BulkImports::Common::Pipelines::WikiPipeline,
stage: 5

View File

@ -84,8 +84,7 @@ module Gitlab
params '~label1 ~"label 2"'
types Issuable
condition do
parent &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent) &&
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target) &&
find_labels.any?
end
command :label do |labels_param|
@ -107,7 +106,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.labels.any? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :unlabel, :remove_label do |labels_param = nil|
if labels_param.present?
@ -139,7 +138,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.labels.any? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :relabel do |labels_param|
run_label_command(labels: find_labels(labels_param), command: :relabel, updates_key: :label_ids)

View File

@ -19,7 +19,7 @@ module Gitlab
types Issue
condition do
quick_action_target.respond_to?(:due_date) &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
parse_params do |due_date_param|
Chronic.parse(due_date_param).try(:to_date)
@ -40,7 +40,7 @@ module Gitlab
quick_action_target.persisted? &&
quick_action_target.respond_to?(:due_date) &&
quick_action_target.due_date? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :remove_due_date do
@updates[:due_date] = nil
@ -54,7 +54,7 @@ module Gitlab
params '~"Target column"'
types Issue
condition do
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) &&
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target) &&
quick_action_target.project.boards.count == 1
end
command :board_move do |target_list_name|
@ -86,7 +86,7 @@ module Gitlab
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :duplicate do |duplicate_param|
canonical_issue = extract_references(duplicate_param, :issue).first

View File

@ -26,7 +26,7 @@ module Gitlab
end
types Issue, MergeRequest
condition do
quick_action_target.supports_assignee? && current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
quick_action_target.supports_assignee? && current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
parse_params do |assignee_param|
extract_users(assignee_param)
@ -66,7 +66,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.assignees.any? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
parse_params do |unassign_param|
# When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
@ -92,7 +92,7 @@ module Gitlab
types Issue, MergeRequest
condition do
quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target) &&
find_milestones(project, state: 'active').any?
end
parse_params do |milestone_param|
@ -115,7 +115,7 @@ module Gitlab
quick_action_target.persisted? &&
quick_action_target.milestone_id? &&
quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :remove_milestone do
@updates[:milestone_id] = nil
@ -128,7 +128,7 @@ module Gitlab
params '#issue | !merge_request'
types Issue, MergeRequest
condition do
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
@ -225,7 +225,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
!quick_action_target.discussion_locked? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :lock do
@updates[:discussion_locked] = true
@ -238,7 +238,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.discussion_locked? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
current_user.can?(:"set_#{quick_action_target.to_ability_name}_metadata", quick_action_target)
end
command :unlock do
@updates[:discussion_locked] = false

View File

@ -70,9 +70,6 @@ msgstr ""
msgid "\"%{repository_name}\" size (%{repository_size}) is larger than the limit of %{limit}."
msgstr ""
msgid "\"el\" parameter is required for createInstance()"
msgstr ""
msgid "#%{issueIid} (closed)"
msgstr ""
@ -32577,9 +32574,6 @@ msgstr ""
msgid "Source Branch"
msgstr ""
msgid "Source Editor instance is required to set up an extension."
msgstr ""
msgid "Source IP"
msgstr ""
@ -32598,6 +32592,15 @@ msgstr ""
msgid "Source project cannot be found."
msgstr ""
msgid "SourceEditor|\"el\" parameter is required for createInstance()"
msgstr ""
msgid "SourceEditor|Extension definition should be either a class or a function"
msgstr ""
msgid "SourceEditor|Source Editor instance is required to set up an extension."
msgstr ""
msgid "Sourcegraph"
msgstr ""
@ -35196,6 +35199,9 @@ msgstr ""
msgid "This field is required."
msgstr ""
msgid "This file was modified for readability, and can't accept suggestions. Edit it directly."
msgstr ""
msgid "This form is disabled in preview"
msgstr ""

View File

@ -124,6 +124,15 @@ module QA
click_element(:more_assignees_link)
end
# When the labels_widget feature flag is enabled, wait until the labels widget appears
def wait_for_labels_widget_feature_flag
Support::Retrier.retry_until(max_duration: 60, reload_page: page, retry_on_exception: true, sleep_interval: 5) do
within_element(:labels_block) do
find_element(:edit_link)
end
end
end
private
def wait_assignees_block_finish_loading

View File

@ -43,7 +43,7 @@ module QA
def with_allow_duplicates_button
within_element :allow_duplicates_toggle do
toggle = find('button.gl-toggle')
toggle = find('button.gl-toggle:not(.is-disabled)')
yield(toggle)
end
end

View File

@ -3,77 +3,56 @@
module QA
RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
describe 'Maven Repository' do
using RSpec::Parameterized::TableSyntax
include Runtime::Fixtures
include_context 'packages registry qa scenario'
let(:group_id) { 'com.gitlab.qa' }
let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
let(:another_artifact_id) { "maven-#{SecureRandom.hex(8)}" }
let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
let(:auth_token) do
unless Page::Main::Menu.perform(&:signed_in?)
Flow::Login.sign_in
end
let(:package_version) { '1.3.7' }
let(:package_type) { 'maven' }
Resource::PersonalAccessToken.fabricate!.token
let(:package_gitlab_ci_file) do
{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
deploy:
image: maven:3.6-jdk-11
script:
- 'mvn deploy -s settings.xml'
only:
- "#{package_project.default_branch}"
tags:
- "runner-for-#{package_project.group.name}"
YAML
}
end
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'maven-package-project'
end
end
let(:another_project) do
Resource::Project.fabricate_via_api! do |another_project|
another_project.name = 'another-maven-package-project'
another_project.group = project.group
end
end
let(:package) do
Resource::Package.init do |package|
package.name = package_name
package.project = project
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.name = "qa-runner-#{Time.now.to_i}"
runner.tags = ["runner-for-#{project.group.name}"]
runner.executor = :docker
runner.token = project.group.runners_token
end
end
let!(:gitlab_address_with_port) do
uri = URI.parse(Runtime::Scenario.gitlab_address)
"#{uri.scheme}://#{uri.host}:#{uri.port}"
end
let(:pom_xml) do
let(:package_pom_file) do
{
file_path: 'pom.xml',
content: <<~XML
<project>
<groupId>#{group_id}</groupId>
<artifactId>#{artifact_id}</artifactId>
<version>1.0</version>
<version>#{package_version}</version>
<modelVersion>4.0.0</modelVersion>
<repositories>
<repository>
<id>#{project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/groups/#{project.group.id}/-/packages/maven</url>
<id>#{package_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>#{project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url>
<id>#{package_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
</repository>
<snapshotRepository>
<id>#{project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url>
<id>#{package_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
</snapshotRepository>
</distributionManagement>
</project>
@ -81,36 +60,43 @@ module QA
}
end
let(:pom_xml_another_project) do
let(:client_gitlab_ci_file) do
{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
install:
image: maven:3.6-jdk-11
script:
- "mvn install -s settings.xml"
only:
- "#{client_project.default_branch}"
tags:
- "runner-for-#{client_project.group.name}"
YAML
}
end
let(:client_pom_file) do
{
file_path: 'pom.xml',
content: <<~XML
<project>
<groupId>#{group_id}</groupId>
<artifactId>#{another_artifact_id}</artifactId>
<artifactId>maven_client</artifactId>
<version>1.0</version>
<modelVersion>4.0.0</modelVersion>
<repositories>
<repository>
<id>#{another_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/groups/#{another_project.group.id}/-/packages/maven</url>
<id>#{package_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>#{another_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{another_project.id}/packages/maven</url>
</repository>
<snapshotRepository>
<id>#{another_project.name}</id>
<url>#{gitlab_address_with_port}/api/v4/projects/#{another_project.id}/packages/maven</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>#{group_id}</groupId>
<artifactId>#{artifact_id}</artifactId>
<version>1.0</version>
<version>#{package_version}</version>
</dependency>
</dependencies>
</project>
@ -118,7 +104,7 @@ module QA
}
end
let(:settings_xml) do
let(:settings_xml_with_pat) do
{
file_path: 'settings.xml',
content: <<~XML
@ -126,12 +112,12 @@ module QA
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
<servers>
<server>
<id>#{project.name}</id>
<id>#{package_project.name}</id>
<configuration>
<httpHeaders>
<property>
<name>Private-Token</name>
<value>#{auth_token}</value>
<value>#{personal_access_token}</value>
</property>
</httpHeaders>
</configuration>
@ -142,188 +128,62 @@ module QA
}
end
let(:gitlab_ci_deploy_yml) do
{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
deploy:
image: maven:3.6-jdk-11
script:
- 'mvn deploy -s settings.xml'
- "mvn dependency:get -Dartifact=#{group_id}:#{artifact_id}:1.0"
only:
- "#{project.default_branch}"
tags:
- "runner-for-#{project.group.name}"
YAML
}
where(:authentication_token_type, :maven_header_name) do
:personal_access_token | 'Private-Token'
:ci_job_token | 'Job-Token'
:project_deploy_token | 'Deploy-Token'
end
let(:gitlab_ci_install_yml) do
{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
install:
image: maven:3.6-jdk-11
script:
- "mvn install"
only:
- "#{project.default_branch}"
tags:
- "runner-for-#{another_project.group.name}"
YAML
}
end
after do
runner.remove_via_api!
project.remove_via_api!
another_project.remove_via_api!
end
it 'pushes and pulls a Maven package via CI and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1627' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
gitlab_ci_deploy_yml,
settings_xml,
pom_xml
])
end
project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('deploy')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = another_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
gitlab_ci_install_yml,
pom_xml_another_project
])
end
another_project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('install')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
project.visit!
Page::Project::Menu.perform(&:click_packages_link)
Page::Project::Packages::Index.perform do |index|
expect(index).to have_package(package_name)
index.click_package(package_name)
end
Page::Project::Packages::Show.perform do |show|
expect(show).to have_package_info(package_name, "1.0")
show.click_delete
end
Page::Project::Packages::Index.perform do |index|
expect(index).to have_content("Package deleted successfully")
expect(index).not_to have_package(package_name)
end
end
context 'when "allow duplicate" setting is disabled' do
before do
Flow::Login.sign_in
project.group.visit!
Page::Group::Menu.perform(&:go_to_package_settings)
Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
end
it 'prevents users from publishing duplicate Maven packages at the group level', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1830' do
with_fixtures([pom_xml, settings_xml]) do |dir|
Service::DockerRun::Maven.new(dir).publish!
with_them do
let(:token) do
case authentication_token_type
when :personal_access_token
personal_access_token
when :ci_job_token
'${env.CI_JOB_TOKEN}'
when :project_deploy_token
project_deploy_token.password
end
end
project.visit!
Page::Project::Menu.perform(&:click_packages_link)
Page::Project::Packages::Index.perform do |index|
expect(index).to have_package(package_name)
end
let(:settings_xml) do
{
file_path: 'settings.xml',
content: <<~XML
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
<servers>
<server>
<id>#{package_project.name}</id>
<configuration>
<httpHeaders>
<property>
<name>#{maven_header_name}</name>
<value>#{token}</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>
XML
}
end
it "pushes and pulls a maven package via maven using #{params[:authentication_token_type]}" do
# pushing
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = another_project
commit.project = package_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
gitlab_ci_deploy_yml,
settings_xml,
pom_xml
])
package_gitlab_ci_file,
package_pom_file,
settings_xml
])
end
another_project.visit!
Flow::Pipeline.visit_latest_pipeline
package_project.visit!
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('deploy')
end
Page::Project::Job::Show.perform do |job|
expect(job).not_to be_successful(timeout: 800)
end
end
end
context 'when "allow duplicate" setting is enabled' do
before do
Flow::Login.sign_in
project.group.visit!
Page::Group::Menu.perform(&:go_to_package_settings)
Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
end
it 'allows users to publish duplicate Maven packages at the group level', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1829' do
with_fixtures([pom_xml, settings_xml]) do |dir|
Service::DockerRun::Maven.new(dir).publish!
end
project.visit!
Page::Project::Menu.perform(&:click_packages_link)
Page::Project::Packages::Index.perform do |index|
expect(index).to have_package(package_name)
end
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = another_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
gitlab_ci_deploy_yml,
settings_xml,
pom_xml
])
end
another_project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
@ -333,6 +193,123 @@ module QA
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
Page::Project::Menu.perform(&:click_packages_link)
Page::Project::Packages::Index.perform do |index|
expect(index).to have_package(package_name)
index.click_package(package_name)
end
Page::Project::Packages::Show.perform do |show|
expect(show).to have_package_info(package_name, package_version)
end
# pulling
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = client_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
client_gitlab_ci_file,
client_pom_file,
settings_xml
])
end
client_project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('install')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
end
context 'duplication setting' do
before do
package_project.group.visit!
Page::Group::Menu.perform(&:go_to_package_settings)
end
context 'when disabled' do
before do
Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
end
it "prevents users from publishing group level Maven packages duplicates using #{params[:authentication_token_type]}" do
create_duplicated_package
push_duplicated_package
client_project.visit!
show_latest_deploy_job
Page::Project::Job::Show.perform do |job|
expect(job).not_to be_successful(timeout: 800)
end
end
end
context 'when enabled' do
before do
Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
end
it "allows users to publish group level Maven packages duplicates using #{params[:authentication_token_type]}" do
create_duplicated_package
push_duplicated_package
show_latest_deploy_job
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
end
end
def create_duplicated_package
with_fixtures([package_pom_file, settings_xml_with_pat]) do |dir|
Service::DockerRun::Maven.new(dir).publish!
end
package_project.visit!
Page::Project::Menu.perform(&:click_packages_link)
Page::Project::Packages::Index.perform do |index|
expect(index).to have_package(package_name)
end
end
def push_duplicated_package
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = client_project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
package_gitlab_ci_file,
package_pom_file,
settings_xml
])
end
end
def show_latest_deploy_job
client_project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('deploy')
end
end
end
end
end

View File

@ -43,7 +43,7 @@ module QA
let(:project_deploy_token) do
Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token|
deploy_token.name = 'helm-package-deploy-token'
deploy_token.name = 'package-deploy-token'
deploy_token.project = package_project
end
end

View File

@ -28,10 +28,6 @@ RSpec.describe 'Group Packages' do
context 'when feature is available', :js do
before do
# we are simply setting the featrure flag to false because the new UI has nothing to test yet
# when the refactor is complete or almost complete we will turn on the feature tests
# see https://gitlab.com/gitlab-org/gitlab/-/issues/330846 for status of this work
stub_feature_flags(package_list_apollo: false)
visit_group_packages
end

View File

@ -27,10 +27,6 @@ RSpec.describe 'Packages' do
context 'when feature is available', :js do
before do
# we are simply setting the featrure flag to false because the new UI has nothing to test yet
# when the refactor is complete or almost complete we will turn on the feature tests
# see https://gitlab.com/gitlab-org/gitlab/-/issues/330846 for status of this work
stub_feature_flags(package_list_apollo: false)
visit_project_packages
end

View File

@ -635,7 +635,7 @@ RSpec.describe 'Pipelines', :js do
# header
expect(page).to have_text("##{pipeline.id}")
expect(page).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"]))
expect(page).to have_selector(%Q(img[src="#{pipeline.user.avatar_url}"]))
expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
# stages

View File

@ -0,0 +1,96 @@
import EditorExtension from '~/editor/source_editor_extension';
import { EDITOR_EXTENSION_DEFINITION_ERROR } from '~/editor/constants';
class MyClassExtension {
// eslint-disable-next-line class-methods-use-this
provides() {
return {
shared: () => 'extension',
classExtMethod: () => 'class own method',
};
}
}
function MyFnExtension() {
return {
fnExtMethod: () => 'fn own method',
provides: () => {
return {
shared: () => 'extension',
};
},
};
}
const MyConstExt = () => {
return {
provides: () => {
return {
shared: () => 'extension',
constExtMethod: () => 'const own method',
};
},
};
};
describe('Editor Extension', () => {
const dummyObj = { foo: 'bar' };
it.each`
definition | setupOptions
${undefined} | ${undefined}
${undefined} | ${{}}
${undefined} | ${dummyObj}
${{}} | ${dummyObj}
${dummyObj} | ${dummyObj}
`(
'throws when definition = $definition and setupOptions = $setupOptions',
({ definition, setupOptions }) => {
const constructExtension = () => new EditorExtension({ definition, setupOptions });
expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR);
},
);
it.each`
definition | setupOptions | expectedName
${MyClassExtension} | ${undefined} | ${'MyClassExtension'}
${MyClassExtension} | ${{}} | ${'MyClassExtension'}
${MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
${MyFnExtension} | ${undefined} | ${'MyFnExtension'}
${MyFnExtension} | ${{}} | ${'MyFnExtension'}
${MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
${MyConstExt} | ${undefined} | ${'MyConstExt'}
${MyConstExt} | ${{}} | ${'MyConstExt'}
${MyConstExt} | ${dummyObj} | ${'MyConstExt'}
`(
'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
({ definition, setupOptions, expectedName }) => {
const extension = new EditorExtension({ definition, setupOptions });
// eslint-disable-next-line new-cap
const constructedDefinition = new definition();
expect(extension).toEqual(
expect.objectContaining({
name: expectedName,
setupOptions,
}),
);
expect(extension.obj.constructor.prototype).toBe(constructedDefinition.constructor.prototype);
},
);
describe('api', () => {
it.each`
definition | expectedKeys
${MyClassExtension} | ${['shared', 'classExtMethod']}
${MyFnExtension} | ${['shared']}
${MyConstExt} | ${['shared', 'constExtMethod']}
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
const extension = new EditorExtension({ definition });
const expectedApi = Object.fromEntries(
expectedKeys.map((key) => [key, expect.any(Function)]),
);
expect(extension.api).toEqual(expect.objectContaining(expectedApi));
});
});
});

View File

@ -15,7 +15,6 @@ import DeletePackage from '~/packages_and_registries/package_registry/components
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
@ -86,15 +85,24 @@ describe('PackagesListApp', () => {
wrapper.destroy();
});
const waitForDebouncedApollo = () => {
jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
const waitForFirstRequest = () => {
// emit a search update so the query is executed
findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
};
it('does not execute the query without sort being set', () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
expect(resolver).not.toHaveBeenCalled();
});
it('renders', async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
expect(wrapper.element).toMatchSnapshot();
});
@ -102,7 +110,7 @@ describe('PackagesListApp', () => {
it('has a package title', async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props('count')).toBe(2);
@ -121,8 +129,7 @@ describe('PackagesListApp', () => {
findSearch().vm.$emit('update', searchPayload);
await waitForDebouncedApollo();
jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
@ -140,7 +147,7 @@ describe('PackagesListApp', () => {
resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
return waitForDebouncedApollo();
return waitForFirstRequest();
});
it('exists and has the right props', () => {
@ -182,7 +189,7 @@ describe('PackagesListApp', () => {
provide = { ...defaultProvide, isGroupPage };
resolver = jest.fn().mockResolvedValue(packagesListQuery({ type }));
mountComponent({ provide, resolver });
return waitForDebouncedApollo();
return waitForFirstRequest();
});
it('succeeds', () => {
@ -191,7 +198,7 @@ describe('PackagesListApp', () => {
it('calls the resolver with the right parameters', () => {
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ isGroupPage, [sortType]: '' }),
expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }),
);
});
});
@ -201,7 +208,7 @@ describe('PackagesListApp', () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
mountComponent({ resolver });
return waitForDebouncedApollo();
return waitForFirstRequest();
});
it('generate the correct empty list link', () => {
const link = findListComponent().findComponent(GlLink);
@ -219,7 +226,7 @@ describe('PackagesListApp', () => {
beforeEach(async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
findSearch().vm.$emit('update', searchPayload);
@ -236,7 +243,7 @@ describe('PackagesListApp', () => {
it('exists and has the correct props', async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
expect(findDeletePackage().props()).toMatchObject({
refetchQueries: [{ query: getPackagesQuery, variables: {} }],
@ -247,7 +254,7 @@ describe('PackagesListApp', () => {
it('deletePackage is bound to package-list package:delete event', async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
findListComponent().vm.$emit('package:delete', { id: 1 });
@ -257,7 +264,7 @@ describe('PackagesListApp', () => {
it('start and end event set loading correctly', async () => {
mountComponent();
await waitForDebouncedApollo();
await waitForFirstRequest();
findDeletePackage().vm.$emit('start');

View File

@ -1,4 +1,4 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { GlButton, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
@ -18,6 +18,7 @@ describe('Header CI Component', () => {
},
time: '2017-05-08T14:57:39.781Z',
user: {
id: 1234,
web_url: 'path',
name: 'Foo',
username: 'foobar',
@ -29,7 +30,7 @@ describe('Header CI Component', () => {
const findIconBadge = () => wrapper.findComponent(CiIconBadge);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlLink);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
@ -64,10 +65,6 @@ describe('Header CI Component', () => {
expect(findTimeAgo().exists()).toBe(true);
});
it('should render user icon and name', () => {
expect(findUserLink().text()).toContain(defaultProps.user.name);
});
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
@ -77,6 +74,45 @@ describe('Header CI Component', () => {
});
});
describe('user avatar', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline' });
});
it('contains the username', () => {
expect(findUserLink().text()).toContain(defaultProps.user.username);
});
it('has the correct data attributes', () => {
expect(findUserLink().attributes()).toMatchObject({
'data-user-id': defaultProps.user.id.toString(),
'data-username': defaultProps.user.username,
'data-name': defaultProps.user.name,
});
});
describe('with data from GraphQL', () => {
const userId = 1;
beforeEach(() => {
createComponent({
itemName: 'Pipeline',
user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` },
});
});
it('has the correct user id', () => {
expect(findUserLink().attributes('data-user-id')).toBe(userId.toString());
});
});
describe('with data from REST', () => {
it('has the correct user id', () => {
expect(findUserLink().attributes('data-user-id')).toBe(defaultProps.user.id.toString());
});
});
});
describe('with item id', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline', itemId: '123' });

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Projects::Pipelines::ProtectedBranchesPipeline do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let_it_be(:protected_branch) do
{
'name' => 'main',
'created_at' => '2016-06-14T15:02:47.967Z',
'updated_at' => '2016-06-14T15:02:47.967Z',
'merge_access_levels' => [
{
'access_level' => 40,
'created_at' => '2016-06-15T15:02:47.967Z',
'updated_at' => '2016-06-15T15:02:47.967Z'
}
],
'push_access_levels' => [
{
'access_level' => 30,
'created_at' => '2016-06-16T15:02:47.967Z',
'updated_at' => '2016-06-16T15:02:47.967Z'
}
]
}
end
subject(:pipeline) { described_class.new(context) }
describe '#run' do
it 'imports protected branch information' do
allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [protected_branch, 0]))
end
pipeline.run
imported_protected_branch = project.protected_branches.last
merge_access_level = imported_protected_branch.merge_access_levels.first
push_access_level = imported_protected_branch.push_access_levels.first
aggregate_failures do
expect(imported_protected_branch.name).to eq(protected_branch['name'])
expect(imported_protected_branch.updated_at).to eq(protected_branch['updated_at'])
expect(imported_protected_branch.created_at).to eq(protected_branch['created_at'])
expect(merge_access_level.access_level).to eq(protected_branch['merge_access_levels'].first['access_level'])
expect(merge_access_level.created_at).to eq(protected_branch['merge_access_levels'].first['created_at'])
expect(merge_access_level.updated_at).to eq(protected_branch['merge_access_levels'].first['updated_at'])
expect(push_access_level.access_level).to eq(protected_branch['push_access_levels'].first['access_level'])
expect(push_access_level.created_at).to eq(protected_branch['push_access_levels'].first['created_at'])
expect(push_access_level.updated_at).to eq(protected_branch['push_access_levels'].first['updated_at'])
end
end
end
end

View File

@ -13,6 +13,7 @@ RSpec.describe BulkImports::Projects::Stage do
[4, BulkImports::Common::Pipelines::BoardsPipeline],
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
[4, BulkImports::Projects::Pipelines::ProtectedBranchesPipeline],
[5, BulkImports::Common::Pipelines::WikiPipeline],
[5, BulkImports::Common::Pipelines::UploadsPipeline],
[6, BulkImports::Common::Pipelines::EntityFinisher]
@ -27,7 +28,7 @@ RSpec.describe BulkImports::Projects::Stage do
describe '#pipelines' do
it 'list all the pipelines with their stage number, ordered by stage' do
expect(subject.pipelines & pipelines).to eq(pipelines)
expect(subject.pipelines & pipelines).to contain_exactly(*pipelines)
expect(subject.pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher)
end
end

View File

@ -857,10 +857,14 @@ RSpec.describe Integrations::Jira do
let_it_be(:user) { build_stubbed(:user) }
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
let(:success_message) { 'SUCCESS: Successfully posted to http://jira.example.com.' }
let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
subject { jira_integration.create_cross_reference_note(jira_issue, resource, user) }
shared_examples 'creates a comment on Jira' do
shared_examples 'handles cross-references' do
let(:resource_name) { jira_integration.send(:noteable_name, resource) }
let(:resource_url) { jira_integration.send(:build_entity_url, resource_name, resource.to_param) }
let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
let(:comment_url) { "#{issue_url}/comment" }
let(:remote_link_url) { "#{issue_url}/remotelink" }
@ -872,12 +876,65 @@ RSpec.describe Integrations::Jira do
stub_request(:post, remote_link_url).with(basic_auth: [username, password])
end
it 'creates a comment on Jira' do
subject
context 'when enabled' do
before do
allow(jira_integration).to receive(:can_cross_reference?) { true }
end
expect(WebMock).to have_requested(:post, comment_url).with(
body: /mentioned this issue.*on branch \[master/
).once
it 'creates a comment and remote link' do
expect(subject).to eq(success_message)
expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
expect(WebMock).to have_requested(:post, remote_link_url).with(
body: hash_including(
GlobalID: 'GitLab',
relationship: 'mentioned on',
object: {
url: resource_url,
title: "#{resource.model_name.human} - #{resource.title}",
icon: { title: 'GitLab', url16x16: favicon_path },
status: { resolved: false }
}
)
).once
end
context 'when comment already exists' do
before do
allow(jira_integration).to receive(:comment_exists?) { true }
end
it 'does not create a comment or remote link' do
expect(subject).to be_nil
expect(WebMock).not_to have_requested(:post, comment_url)
expect(WebMock).not_to have_requested(:post, remote_link_url)
end
end
context 'when remote link already exists' do
let(:link) { double(object: { 'url' => resource_url }) }
before do
allow(jira_integration).to receive(:find_remote_link).and_return(link)
end
it 'updates the remote link but does not create a comment' do
expect(link).to receive(:save!)
expect(subject).to eq(success_message)
expect(WebMock).not_to have_requested(:post, comment_url)
end
end
end
context 'when disabled' do
before do
allow(jira_integration).to receive(:can_cross_reference?) { false }
end
it 'does not create a comment or remote link' do
expect(subject).to eq("Events for #{resource_name.pluralize.humanize(capitalize: false)} are disabled.")
expect(WebMock).not_to have_requested(:post, comment_url)
expect(WebMock).not_to have_requested(:post, remote_link_url)
end
end
context 'with jira_use_first_ref_by_oid feature flag disabled' do
@ -885,12 +942,10 @@ RSpec.describe Integrations::Jira do
stub_feature_flags(jira_use_first_ref_by_oid: false)
end
it 'creates a comment on Jira' do
subject
expect(WebMock).to have_requested(:post, comment_url).with(
body: /mentioned this issue.*on branch \[master/
).once
it 'creates a comment and remote link on Jira' do
expect(subject).to eq(success_message)
expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
expect(WebMock).to have_requested(:post, remote_link_url).once
end
end
@ -903,39 +958,38 @@ RSpec.describe Integrations::Jira do
end
end
context 'when resource is a commit' do
let(:resource) { project.commit('master') }
context 'when disabled' do
before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:commit_events) { false }
end
end
it { is_expected.to eq('Events for commits are disabled.') }
end
context 'when enabled' do
it_behaves_like 'creates a comment on Jira'
context 'for commits' do
it_behaves_like 'handles cross-references' do
let(:resource) { project.commit('master') }
let(:comment_body) { /mentioned this issue in \[a commit\|.* on branch \[master\|/ }
end
end
context 'when resource is a merge request' do
let(:resource) { build_stubbed(:merge_request, source_project: project) }
context 'when disabled' do
before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:merge_requests_events) { false }
end
end
it { is_expected.to eq('Events for merge requests are disabled.') }
context 'for issues' do
it_behaves_like 'handles cross-references' do
let(:resource) { build_stubbed(:issue, project: project) }
let(:comment_body) { /mentioned this issue in \[a issue\|/ }
end
end
context 'when enabled' do
it_behaves_like 'creates a comment on Jira'
context 'for merge requests' do
it_behaves_like 'handles cross-references' do
let(:resource) { build_stubbed(:merge_request, source_project: project) }
let(:comment_body) { /mentioned this issue in \[a merge request\|.* on branch \[master\|/ }
end
end
context 'for notes' do
it_behaves_like 'handles cross-references' do
let(:resource) { build_stubbed(:note, project: project) }
let(:comment_body) { /mentioned this issue in \[a note\|/ }
end
end
context 'for snippets' do
it_behaves_like 'handles cross-references' do
let(:resource) { build_stubbed(:snippet, project: project) }
let(:comment_body) { /mentioned this issue in \[a snippet\|/ }
end
end
end

View File

@ -154,6 +154,14 @@ RSpec.describe Suggestion do
it { is_expected.to eq("This suggestion already matches its content.") }
end
context 'when file is .ipynb' do
before do
allow(suggestion).to receive(:file_path).and_return("example.ipynb")
end
it { is_expected.to eq(_("This file was modified for readability, and can't accept suggestions. Edit it directly.")) }
end
context 'when applicable' do
it { is_expected.to be_nil }
end

View File

@ -2553,4 +2553,32 @@ RSpec.describe QuickActions::InterpretService do
end
end
end
describe '#available_commands' do
context 'when Guest is creating a new issue' do
let_it_be(:guest) { create(:user) }
let(:issue) { build(:issue, project: public_project) }
let(:service) { described_class.new(project, guest) }
before_all do
public_project.add_guest(guest)
end
it 'includes commands to set metadata' do
# milestone action is only available when project has a milestone
milestone
available_commands = service.available_commands(issue)
expect(available_commands).to include(
a_hash_including(name: :label),
a_hash_including(name: :milestone),
a_hash_including(name: :copy_metadata),
a_hash_including(name: :assign),
a_hash_including(name: :due)
)
end
end
end
end

View File

@ -348,193 +348,6 @@ RSpec.describe SystemNoteService do
end
end
describe 'Jira integration' do
include JiraServiceHelper
let(:project) { create(:jira_project, :repository) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.jira_integration }
let(:commit) { project.commit }
let(:comment_url) { jira_api_comment_url(jira_issue.id) }
let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." }
before do
stub_jira_integration_test
stub_jira_urls(jira_issue.id)
jira_integration_settings
end
def cross_reference(type, link_exists = false)
noteable = type == 'commit' ? commit : merge_request
links = []
if link_exists
url = if type == 'commit'
"#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/-/commit/#{commit.id}"
else
"#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/-/merge_requests/#{merge_request.iid}"
end
link = double(object: { 'url' => url })
links << link
expect(link).to receive(:save!)
end
allow(JIRA::Resource::Remotelink).to receive(:all).and_return(links)
described_class.cross_reference(jira_issue, noteable, author)
end
noteable_types = %w(merge_requests commit)
noteable_types.each do |type|
context "when noteable is a #{type}" do
it "blocks cross reference when #{type.underscore}_events is false" do
jira_tracker.update!("#{type}_events" => false)
expect(cross_reference(type)).to eq(s_('JiraService|Events for %{noteable_model_name} are disabled.') % { noteable_model_name: type.pluralize.humanize.downcase })
end
it "creates cross reference when #{type.underscore}_events is true" do
jira_tracker.update!("#{type}_events" => true)
expect(cross_reference(type)).to eq(success_message)
end
end
context 'when a new cross reference is created' do
it 'creates a new comment and remote link' do
cross_reference(type)
expect(WebMock).to have_requested(:post, jira_api_comment_url(jira_issue))
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue))
end
end
context 'when a link exists' do
it 'updates a link but does not create a new comment' do
expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
cross_reference(type, true)
end
end
end
describe "new reference" do
let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
end
context 'for commits' do
it "creates comment" do
result = described_class.cross_reference(jira_issue, commit, author)
expect(result).to eq(success_message)
end
it "creates remote link" do
described_class.cross_reference(jira_issue, commit, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including(
GlobalID: "GitLab",
relationship: 'mentioned on',
object: {
url: project_commit_url(project, commit),
title: "Commit - #{commit.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
).once
end
end
context 'for issues' do
let(:issue) { create(:issue, project: project) }
it "creates comment" do
result = described_class.cross_reference(jira_issue, issue, author)
expect(result).to eq(success_message)
end
it "creates remote link" do
described_class.cross_reference(jira_issue, issue, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including(
GlobalID: "GitLab",
relationship: 'mentioned on',
object: {
url: project_issue_url(project, issue),
title: "Issue - #{issue.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
).once
end
end
context 'for snippets' do
let(:snippet) { create(:snippet, project: project) }
it "creates comment" do
result = described_class.cross_reference(jira_issue, snippet, author)
expect(result).to eq(success_message)
end
it "creates remote link" do
described_class.cross_reference(jira_issue, snippet, author)
expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
body: hash_including(
GlobalID: "GitLab",
relationship: 'mentioned on',
object: {
url: project_snippet_url(project, snippet),
title: "Snippet - #{snippet.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
).once
end
end
end
describe "existing reference" do
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
message = double('message')
allow(message).to receive(:include?) { true }
allow_next_instance_of(JIRA::Resource::Issue) do |instance|
allow(instance).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
end
it "does not return success message" do
result = described_class.cross_reference(jira_issue, commit, author)
expect(result).not_to eq(success_message)
end
it 'does not try to create comment and remote link' do
subject
expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
end
end
end
describe '.change_time_estimate' do
it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|

View File

@ -347,6 +347,23 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
end
context 'with external issue' do
let(:noteable) { ExternalIssue.new('JIRA-123', project) }
let(:mentioner) { project.commit }
it 'queues a background worker' do
expect(Integrations::CreateExternalCrossReferenceWorker).to receive(:perform_async).with(
project.id,
'JIRA-123',
'Commit',
mentioner.id,
author.id
)
subject
end
end
end
end

View File

@ -21,10 +21,6 @@ end
RSpec.shared_examples 'package details link' do |property|
let(:package) { packages.first }
before do
stub_feature_flags(packages_details_one_column: false)
end
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
@ -94,16 +90,24 @@ def packages_table_selector
end
def click_sort_option(option, ascending)
page.within('.gl-sorting') do
# Reset the sort direction
click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0)
wait_for_requests
find('button.gl-dropdown-toggle').click
# Reset the sort direction
if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending
click_button 'Sort direction'
page.within('.dropdown-menu') do
click_button option
end
wait_for_requests
end
click_button 'Sort direction' if ascending
find('button.gl-dropdown-toggle').click
page.within('.dropdown-menu') do
click_button option
end
if ascending
wait_for_requests
click_button 'Sort direction'
end
end

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::CreateExternalCrossReferenceWorker do
include AfterNextHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:jira_project, :repository) }
let_it_be(:author) { create(:user) }
let_it_be(:commit) { project.commit }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:note) { create(:note, project: project) }
let_it_be(:snippet) { create(:project_snippet, project: project) }
let(:project_id) { project.id }
let(:external_issue_id) { 'JIRA-123' }
let(:mentionable_type) { 'Issue' }
let(:mentionable_id) { issue.id }
let(:author_id) { author.id }
let(:job_args) { [project_id, external_issue_id, mentionable_type, mentionable_id, author_id] }
def perform
described_class.new.perform(*job_args)
end
before do
allow(Project).to receive(:find_by_id).and_return(project)
end
it_behaves_like 'an idempotent worker' do
before do
allow(project.external_issue_tracker).to receive(:create_cross_reference_note)
end
it 'can run multiple times with the same arguments' do
subject
expect(project.external_issue_tracker).to have_received(:create_cross_reference_note)
.exactly(worker_exec_times).times
end
end
it 'has the `until_executed` deduplicate strategy' do
expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
expect(described_class.get_deduplication_options).to include({ including_scheduled: true })
end
# These are the only models where we currently support cross-references,
# although this should be expanded to all `Mentionable` models.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/343975
where(:mentionable_type, :mentionable_id) do
'Commit' | lazy { commit.id }
'Issue' | lazy { issue.id }
'MergeRequest' | lazy { merge_request.id }
'Note' | lazy { note.id }
'Snippet' | lazy { snippet.id }
end
with_them do
it 'creates a cross reference' do
expect(project.external_issue_tracker).to receive(:create_cross_reference_note).with(
be_a(ExternalIssue).and(have_attributes(id: external_issue_id, project: project)),
be_a(mentionable_type.constantize).and(have_attributes(id: mentionable_id)),
be_a(User).and(have_attributes(id: author_id))
)
perform
end
end
describe 'error handling' do
shared_examples 'does not create a cross reference' do
it 'does not create a cross reference' do
expect(project).not_to receive(:external_issue_tracker) if project
perform
end
end
context 'project_id does not exist' do
let(:project_id) { non_existing_record_id }
let(:project) { nil }
it_behaves_like 'does not create a cross reference'
end
context 'author_id does not exist' do
let(:author_id) { non_existing_record_id }
it_behaves_like 'does not create a cross reference'
end
context 'mentionable_id does not exist' do
let(:mentionable_id) { non_existing_record_id }
it_behaves_like 'does not create a cross reference'
end
context 'mentionable_type is not a Mentionable' do
let(:mentionable_type) { 'User' }
before do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(kind_of(ArgumentError))
end
it_behaves_like 'does not create a cross reference'
end
context 'mentionable_type is not a defined constant' do
let(:mentionable_type) { 'FooBar' }
before do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(kind_of(ArgumentError))
end
it_behaves_like 'does not create a cross reference'
end
context 'mentionable is a Commit and mentionable_id does not exist' do
let(:mentionable_type) { 'Commit' }
let(:mentionable_id) { non_existing_record_id }
it_behaves_like 'does not create a cross reference'
end
end
end