Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6fd750c192
commit
7b2f941669
|
|
@ -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 #
|
||||
#######################################################
|
||||
|
|
|
|||
|
|
@ -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 #
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
|
|||
}
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
name
|
||||
username
|
||||
webPath
|
||||
webUrl
|
||||
email
|
||||
avatarUrl
|
||||
status {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -201,6 +201,8 @@
|
|||
- 1
|
||||
- - incident_management_pending_escalations_alert_create
|
||||
- 1
|
||||
- - integrations_create_external_cross_reference
|
||||
- 1
|
||||
- - invalid_gpg_signature_update
|
||||
- 2
|
||||
- - irker
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -55,6 +55,7 @@ exceptions:
|
|||
- FAQ
|
||||
- FIFO
|
||||
- FIPS
|
||||
- FLAG
|
||||
- FOSS
|
||||
- FQDN
|
||||
- FREE
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|-------------|-------------|------------------|------------------|-------------|
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue