Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
915a5b6e89
commit
19c9422e1f
28 changed files with 961 additions and 677 deletions
|
@ -19,6 +19,9 @@
|
||||||
.if-default-branch-refs: &if-default-branch-refs
|
.if-default-branch-refs: &if-default-branch-refs
|
||||||
if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'
|
if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'
|
||||||
|
|
||||||
|
.if-stable-branch-refs: &if-stable-branch-refs
|
||||||
|
if: '$CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/'
|
||||||
|
|
||||||
.if-default-branch-push: &if-default-branch-push
|
.if-default-branch-push: &if-default-branch-push
|
||||||
if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
|
if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
|
||||||
|
|
||||||
|
@ -581,6 +584,8 @@
|
||||||
when: never
|
when: never
|
||||||
- <<: *if-merge-request-targeting-stable-branch
|
- <<: *if-merge-request-targeting-stable-branch
|
||||||
when: never
|
when: never
|
||||||
|
- <<: *if-stable-branch-refs
|
||||||
|
when: never
|
||||||
- <<: *if-merge-request-labels-as-if-jh
|
- <<: *if-merge-request-labels-as-if-jh
|
||||||
- <<: *if-merge-request-labels-run-all-rspec
|
- <<: *if-merge-request-labels-run-all-rspec
|
||||||
- changes: *code-backstage-qa-patterns
|
- changes: *code-backstage-qa-patterns
|
||||||
|
@ -616,6 +621,8 @@
|
||||||
when: never
|
when: never
|
||||||
- <<: *if-merge-request-targeting-stable-branch
|
- <<: *if-merge-request-targeting-stable-branch
|
||||||
when: never
|
when: never
|
||||||
|
- <<: *if-stable-branch-refs
|
||||||
|
when: never
|
||||||
- <<: *if-merge-request-labels-as-if-jh
|
- <<: *if-merge-request-labels-as-if-jh
|
||||||
- <<: *if-merge-request-labels-run-all-rspec
|
- <<: *if-merge-request-labels-run-all-rspec
|
||||||
- <<: *if-merge-request
|
- <<: *if-merge-request
|
||||||
|
@ -1259,6 +1266,8 @@
|
||||||
when: never
|
when: never
|
||||||
- <<: *if-merge-request-targeting-stable-branch
|
- <<: *if-merge-request-targeting-stable-branch
|
||||||
when: never
|
when: never
|
||||||
|
- <<: *if-stable-branch-refs
|
||||||
|
when: never
|
||||||
- <<: *if-merge-request-labels-as-if-jh
|
- <<: *if-merge-request-labels-as-if-jh
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
- <<: *if-merge-request
|
- <<: *if-merge-request
|
||||||
|
@ -1714,6 +1723,8 @@
|
||||||
when: never
|
when: never
|
||||||
- <<: *if-merge-request-targeting-stable-branch
|
- <<: *if-merge-request-targeting-stable-branch
|
||||||
when: never
|
when: never
|
||||||
|
- <<: *if-stable-branch-refs
|
||||||
|
when: never
|
||||||
- <<: *if-merge-request-labels-as-if-jh
|
- <<: *if-merge-request-labels-as-if-jh
|
||||||
- <<: *if-merge-request-labels-run-all-rspec
|
- <<: *if-merge-request-labels-run-all-rspec
|
||||||
- changes: *code-backstage-qa-patterns
|
- changes: *code-backstage-qa-patterns
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import './autosize';
|
import './autosize';
|
||||||
import './bind_in_out';
|
|
||||||
import './markdown/render_gfm';
|
import './markdown/render_gfm';
|
||||||
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
|
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
|
||||||
import initCopyToClipboard from './copy_to_clipboard';
|
import initCopyToClipboard from './copy_to_clipboard';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { sortBy, cloneDeep } from 'lodash';
|
import { sortBy, cloneDeep } from 'lodash';
|
||||||
import { isGid } from '~/graphql_shared/utils';
|
import { isGid } from '~/graphql_shared/utils';
|
||||||
import { ListType, MilestoneIDs } from './constants';
|
import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants';
|
||||||
|
|
||||||
export function getMilestone() {
|
export function getMilestone() {
|
||||||
return null;
|
return null;
|
||||||
|
@ -186,6 +186,7 @@ export function isListDraggable(list) {
|
||||||
export const FiltersInfo = {
|
export const FiltersInfo = {
|
||||||
assigneeUsername: {
|
assigneeUsername: {
|
||||||
negatedSupport: true,
|
negatedSupport: true,
|
||||||
|
remap: (k, v) => (v === AssigneeFilterType.any ? 'assigneeWildcardId' : k),
|
||||||
},
|
},
|
||||||
assigneeId: {
|
assigneeId: {
|
||||||
// assigneeId should be renamed to assigneeWildcardId.
|
// assigneeId should be renamed to assigneeWildcardId.
|
||||||
|
@ -204,6 +205,11 @@ export const FiltersInfo = {
|
||||||
},
|
},
|
||||||
milestoneTitle: {
|
milestoneTitle: {
|
||||||
negatedSupport: true,
|
negatedSupport: true,
|
||||||
|
remap: (k, v) => (Object.values(MilestoneFilterType).includes(v) ? 'milestoneWildcardId' : k),
|
||||||
|
},
|
||||||
|
milestoneWildcardId: {
|
||||||
|
negatedSupport: true,
|
||||||
|
transform: (val) => val.toUpperCase(),
|
||||||
},
|
},
|
||||||
myReactionEmoji: {
|
myReactionEmoji: {
|
||||||
negatedSupport: true,
|
negatedSupport: true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { pickBy, isEmpty } from 'lodash';
|
import { pickBy, isEmpty } from 'lodash';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
|
||||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
|
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
|
@ -80,7 +80,7 @@ export default {
|
||||||
|
|
||||||
if (milestoneTitle) {
|
if (milestoneTitle) {
|
||||||
filteredSearchValue.push({
|
filteredSearchValue.push({
|
||||||
type: 'milestone_title',
|
type: 'milestone',
|
||||||
value: { data: milestoneTitle, operator: '=' },
|
value: { data: milestoneTitle, operator: '=' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ export default {
|
||||||
|
|
||||||
if (this.filterParams['not[milestoneTitle]']) {
|
if (this.filterParams['not[milestoneTitle]']) {
|
||||||
filteredSearchValue.push({
|
filteredSearchValue.push({
|
||||||
type: 'milestone_title',
|
type: 'milestone',
|
||||||
value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
|
value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -242,7 +242,7 @@ export default {
|
||||||
search,
|
search,
|
||||||
types,
|
types,
|
||||||
weight,
|
weight,
|
||||||
epic_id: getIdFromGraphQLId(epicId),
|
epic_id: isGid(epicId) ? getIdFromGraphQLId(epicId) : epicId,
|
||||||
my_reaction_emoji: myReactionEmoji,
|
my_reaction_emoji: myReactionEmoji,
|
||||||
release_tag: releaseTag,
|
release_tag: releaseTag,
|
||||||
};
|
};
|
||||||
|
@ -293,7 +293,7 @@ export default {
|
||||||
case 'label_name':
|
case 'label_name':
|
||||||
labels.push(filter.value.data);
|
labels.push(filter.value.data);
|
||||||
break;
|
break;
|
||||||
case 'milestone_title':
|
case 'milestone':
|
||||||
filterParams.milestoneTitle = filter.value.data;
|
filterParams.milestoneTitle = filter.value.data;
|
||||||
break;
|
break;
|
||||||
case 'iteration':
|
case 'iteration':
|
||||||
|
@ -326,6 +326,7 @@ export default {
|
||||||
if (plainText.length) {
|
if (plainText.length) {
|
||||||
filterParams.search = plainText.join(' ');
|
filterParams.search = plainText.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterParams;
|
return filterParams;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { TYPE_USER } from '~/graphql_shared/constants';
|
||||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import {
|
import {
|
||||||
DEFAULT_MILESTONES_GRAPHQL,
|
|
||||||
TOKEN_TITLE_MY_REACTION,
|
TOKEN_TITLE_MY_REACTION,
|
||||||
OPERATOR_IS_AND_IS_NOT,
|
OPERATOR_IS_AND_IS_NOT,
|
||||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
|
@ -136,13 +135,12 @@ export default {
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
type: 'milestone_title',
|
type: 'milestone',
|
||||||
title: milestone,
|
title: milestone,
|
||||||
icon: 'clock',
|
icon: 'clock',
|
||||||
symbol: '%',
|
symbol: '%',
|
||||||
token: MilestoneToken,
|
token: MilestoneToken,
|
||||||
unique: true,
|
unique: true,
|
||||||
defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
|
|
||||||
fetchMilestones: this.fetchMilestones,
|
fetchMilestones: this.fetchMilestones,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -106,6 +106,7 @@ export const FilterFields = {
|
||||||
'authorUsername',
|
'authorUsername',
|
||||||
'labelName',
|
'labelName',
|
||||||
'milestoneTitle',
|
'milestoneTitle',
|
||||||
|
'milestoneWildcardId',
|
||||||
'myReactionEmoji',
|
'myReactionEmoji',
|
||||||
'releaseTag',
|
'releaseTag',
|
||||||
'search',
|
'search',
|
||||||
|
@ -114,6 +115,18 @@ export const FilterFields = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @gitlab/require-i18n-strings */
|
||||||
|
export const AssigneeFilterType = {
|
||||||
|
any: 'Any',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MilestoneFilterType = {
|
||||||
|
any: 'Any',
|
||||||
|
none: 'None',
|
||||||
|
started: 'Started',
|
||||||
|
upcoming: 'Upcoming',
|
||||||
|
};
|
||||||
|
|
||||||
export const DraggableItemTypes = {
|
export const DraggableItemTypes = {
|
||||||
card: 'card',
|
card: 'card',
|
||||||
list: 'list',
|
list: 'list',
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||||
import { s__, n__ } from '~/locale';
|
import { s__, n__ } from '~/locale';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MRWidgetRelatedLinks',
|
name: 'MRWidgetRelatedLinks',
|
||||||
|
directives: {
|
||||||
|
SafeHtml,
|
||||||
|
},
|
||||||
mixins: [glFeatureFlagMixin()],
|
mixins: [glFeatureFlagMixin()],
|
||||||
props: {
|
props: {
|
||||||
relatedLinks: {
|
relatedLinks: {
|
||||||
|
@ -43,14 +47,14 @@ export default {
|
||||||
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
|
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
|
||||||
>
|
>
|
||||||
{{ closesText }}
|
{{ closesText }}
|
||||||
<span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
|
<span v-safe-html="relatedLinks.closing"></span>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="relatedLinks.mentioned"
|
v-if="relatedLinks.mentioned"
|
||||||
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
|
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
|
||||||
>
|
>
|
||||||
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
|
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
|
||||||
<span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
|
<span v-safe-html="relatedLinks.mentioned"></span>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="relatedLinks.assignToMe && showAssignToMe"
|
v-if="relatedLinks.assignToMe && showAssignToMe"
|
||||||
|
|
|
@ -28,13 +28,6 @@ export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
|
||||||
{ value: FILTER_STARTED, text: __('Started'), title: __('Started') },
|
{ value: FILTER_STARTED, text: __('Started'), title: __('Started') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DEFAULT_MILESTONES_GRAPHQL = [
|
|
||||||
{ value: 'any', text: __('Any'), title: __('Any') },
|
|
||||||
{ value: 'none', text: __('None'), title: __('None') },
|
|
||||||
{ value: '#upcoming', text: __('Upcoming'), title: __('Upcoming') },
|
|
||||||
{ value: '#started', text: __('Started'), title: __('Started') },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const SortDirection = {
|
export const SortDirection = {
|
||||||
descending: 'descending',
|
descending: 'descending',
|
||||||
ascending: 'ascending',
|
ascending: 'ascending',
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Ci
|
||||||
|
|
||||||
def initialize(model, update_params)
|
def initialize(model, update_params)
|
||||||
@model = model
|
@model = model
|
||||||
@update_params = update_params
|
@update_params = update_params.symbolize_keys
|
||||||
|
|
||||||
validations!
|
validations!
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,11 +29,11 @@ module Groups
|
||||||
update_group_attributes
|
update_group_attributes
|
||||||
ensure_ownership
|
ensure_ownership
|
||||||
update_integrations
|
update_integrations
|
||||||
update_pending_builds!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
post_update_hooks(@updated_project_ids)
|
post_update_hooks(@updated_project_ids)
|
||||||
propagate_integrations
|
propagate_integrations
|
||||||
|
update_pending_builds
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -228,13 +228,19 @@ module Groups
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_pending_builds!
|
def update_pending_builds
|
||||||
update_params = {
|
if Feature.enabled?(:ci_pending_builds_async_update, default_enabled: :yaml)
|
||||||
|
::Ci::PendingBuilds::UpdateGroupWorker.perform_async(group.id, pending_builds_params)
|
||||||
|
else
|
||||||
|
::Ci::UpdatePendingBuildService.new(group, pending_builds_params).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending_builds_params
|
||||||
|
{
|
||||||
namespace_traversal_ids: group.traversal_ids,
|
namespace_traversal_ids: group.traversal_ids,
|
||||||
namespace_id: group.id
|
namespace_id: group.id
|
||||||
}
|
}
|
||||||
|
|
||||||
::Ci::UpdatePendingBuildService.new(group, update_params).execute
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -104,10 +104,10 @@ module Projects
|
||||||
update_repository_configuration(@new_path)
|
update_repository_configuration(@new_path)
|
||||||
|
|
||||||
execute_system_hooks
|
execute_system_hooks
|
||||||
|
|
||||||
update_pending_builds!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
update_pending_builds
|
||||||
|
|
||||||
post_update_hooks(project)
|
post_update_hooks(project)
|
||||||
rescue Exception # rubocop:disable Lint/RescueException
|
rescue Exception # rubocop:disable Lint/RescueException
|
||||||
rollback_side_effects
|
rollback_side_effects
|
||||||
|
@ -244,13 +244,19 @@ module Projects
|
||||||
Integration.create_from_active_default_integrations(project, :project_id)
|
Integration.create_from_active_default_integrations(project, :project_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_pending_builds!
|
def update_pending_builds
|
||||||
update_params = {
|
if Feature.enabled?(:ci_pending_builds_async_update, default_enabled: :yaml)
|
||||||
|
::Ci::PendingBuilds::UpdateProjectWorker.perform_async(project.id, pending_builds_params)
|
||||||
|
else
|
||||||
|
::Ci::UpdatePendingBuildService.new(project, pending_builds_params).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending_builds_params
|
||||||
|
{
|
||||||
namespace_id: new_namespace.id,
|
namespace_id: new_namespace.id,
|
||||||
namespace_traversal_ids: new_namespace.traversal_ids
|
namespace_traversal_ids: new_namespace.traversal_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
::Ci::UpdatePendingBuildService.new(project, update_params).execute
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1447,6 +1447,24 @@
|
||||||
:weight: 1
|
:weight: 1
|
||||||
:idempotent: true
|
:idempotent: true
|
||||||
:tags: []
|
:tags: []
|
||||||
|
- :name: pipeline_background:ci_pending_builds_update_group
|
||||||
|
:worker_name: Ci::PendingBuilds::UpdateGroupWorker
|
||||||
|
:feature_category: :continuous_integration
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent: true
|
||||||
|
:tags: []
|
||||||
|
- :name: pipeline_background:ci_pending_builds_update_project
|
||||||
|
:worker_name: Ci::PendingBuilds::UpdateProjectWorker
|
||||||
|
:feature_category: :continuous_integration
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent: true
|
||||||
|
:tags: []
|
||||||
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
|
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
|
||||||
:worker_name: Ci::PipelineArtifacts::CoverageReportWorker
|
:worker_name: Ci::PipelineArtifacts::CoverageReportWorker
|
||||||
:feature_category: :code_testing
|
:feature_category: :code_testing
|
||||||
|
|
19
app/workers/ci/pending_builds/update_group_worker.rb
Normal file
19
app/workers/ci/pending_builds/update_group_worker.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
module PendingBuilds
|
||||||
|
class UpdateGroupWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
data_consistency :always
|
||||||
|
idempotent!
|
||||||
|
|
||||||
|
def perform(group_id, update_params)
|
||||||
|
::Group.find_by_id(group_id).try do |group|
|
||||||
|
::Ci::UpdatePendingBuildService.new(group, update_params).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
app/workers/ci/pending_builds/update_project_worker.rb
Normal file
19
app/workers/ci/pending_builds/update_project_worker.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
module PendingBuilds
|
||||||
|
class UpdateProjectWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
data_consistency :always
|
||||||
|
idempotent!
|
||||||
|
|
||||||
|
def perform(project_id, update_params)
|
||||||
|
::Project.find_by_id(project_id).try do |project|
|
||||||
|
::Ci::UpdatePendingBuildService.new(project, update_params).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: ci_pending_builds_async_update
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75197
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346641
|
||||||
|
milestone: '14.6'
|
||||||
|
type: development
|
||||||
|
group: group::pipeline execution
|
||||||
|
default_enabled: false
|
|
@ -6745,6 +6745,9 @@ msgstr ""
|
||||||
msgid "Checkout|%{name}'s storage subscription"
|
msgid "Checkout|%{name}'s storage subscription"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Checkout|%{quantity} CI minutes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Checkout|%{quantity} GB of storage"
|
msgid "Checkout|%{quantity} GB of storage"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -6759,9 +6762,6 @@ msgstr ""
|
||||||
msgid "Checkout|%{startDate} - %{endDate}"
|
msgid "Checkout|%{startDate} - %{endDate}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Checkout|%{totalCiMinutes} CI minutes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})"
|
msgid "Checkout|(may be %{linkStart}charged upon purchase%{linkEnd})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,7 @@ RSpec.describe 'Issue board filters', :js do
|
||||||
|
|
||||||
describe 'filters by releases' do
|
describe 'filters by releases' do
|
||||||
before do
|
before do
|
||||||
filter_input.click
|
set_filter('release')
|
||||||
filter_input.set('release:')
|
|
||||||
filter_first_suggestion.click # Select `=` operator
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'loads all the releases when opened and submit one as filter', :aggregate_failures do
|
it 'loads all the releases when opened and submit one as filter', :aggregate_failures do
|
||||||
|
@ -47,6 +45,35 @@ RSpec.describe 'Issue board filters', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'filters by milestone' do
|
||||||
|
before do
|
||||||
|
set_filter('milestone')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'loads all the milestones when opened and submit one as filter', :aggregate_failures do
|
||||||
|
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 2)
|
||||||
|
|
||||||
|
expect_filtered_search_dropdown_results(filter_dropdown, 6)
|
||||||
|
expect(filter_dropdown).to have_content('None')
|
||||||
|
expect(filter_dropdown).to have_content('Any')
|
||||||
|
expect(filter_dropdown).to have_content('Started')
|
||||||
|
expect(filter_dropdown).to have_content('Upcoming')
|
||||||
|
expect(filter_dropdown).to have_content(milestone_1.title)
|
||||||
|
expect(filter_dropdown).to have_content(milestone_2.title)
|
||||||
|
|
||||||
|
click_on milestone_1.title
|
||||||
|
filter_submit.click
|
||||||
|
|
||||||
|
expect(find('.board:nth-child(1)')).to have_selector('.board-card', count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_filter(filter)
|
||||||
|
filter_input.click
|
||||||
|
filter_input.set("#{filter}:")
|
||||||
|
filter_first_suggestion.click # Select `=` operator
|
||||||
|
end
|
||||||
|
|
||||||
def expect_filtered_search_dropdown_results(filter_dropdown, count)
|
def expect_filtered_search_dropdown_results(filter_dropdown, count)
|
||||||
expect(filter_dropdown).to have_selector('.gl-new-dropdown-item', count: count)
|
expect(filter_dropdown).to have_selector('.gl-new-dropdown-item', count: count)
|
||||||
end
|
end
|
||||||
|
|
|
@ -120,7 +120,7 @@ describe('BoardFilteredSearch', () => {
|
||||||
{ type: 'author_username', value: { data: 'root', operator: '=' } },
|
{ type: 'author_username', value: { data: 'root', operator: '=' } },
|
||||||
{ type: 'label_name', value: { data: 'label', operator: '=' } },
|
{ type: 'label_name', value: { data: 'label', operator: '=' } },
|
||||||
{ type: 'label_name', value: { data: 'label2', operator: '=' } },
|
{ type: 'label_name', value: { data: 'label2', operator: '=' } },
|
||||||
{ type: 'milestone_title', value: { data: 'New Milestone', operator: '=' } },
|
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
|
||||||
{ type: 'types', value: { data: 'INCIDENT', operator: '=' } },
|
{ type: 'types', value: { data: 'INCIDENT', operator: '=' } },
|
||||||
{ type: 'weight', value: { data: '2', operator: '=' } },
|
{ type: 'weight', value: { data: '2', operator: '=' } },
|
||||||
{ type: 'iteration', value: { data: '3341', operator: '=' } },
|
{ type: 'iteration', value: { data: '3341', operator: '=' } },
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
|
||||||
import { keyBy } from 'lodash';
|
import { keyBy } from 'lodash';
|
||||||
import { ListType } from '~/boards/constants';
|
import { ListType } from '~/boards/constants';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
|
|
||||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||||
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
|
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
|
||||||
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
||||||
|
@ -599,10 +598,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji)
|
||||||
icon: 'clock',
|
icon: 'clock',
|
||||||
title: __('Milestone'),
|
title: __('Milestone'),
|
||||||
symbol: '%',
|
symbol: '%',
|
||||||
type: 'milestone_title',
|
type: 'milestone',
|
||||||
token: MilestoneToken,
|
token: MilestoneToken,
|
||||||
unique: true,
|
unique: true,
|
||||||
defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
|
|
||||||
fetchMilestones,
|
fetchMilestones,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,10 +11,7 @@ import createFlash from '~/flash';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
|
||||||
|
|
||||||
import {
|
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
DEFAULT_MILESTONES,
|
|
||||||
DEFAULT_MILESTONES_GRAPHQL,
|
|
||||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
|
||||||
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
||||||
|
|
||||||
import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
|
import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
|
||||||
|
@ -199,12 +196,12 @@ describe('MilestoneToken', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = createComponent({
|
wrapper = createComponent({
|
||||||
active: true,
|
active: true,
|
||||||
config: { ...mockMilestoneToken, defaultMilestones: DEFAULT_MILESTONES_GRAPHQL },
|
config: { ...mockMilestoneToken, defaultMilestones: DEFAULT_MILESTONES },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds the correct value from the activeToken', () => {
|
it('finds the correct value from the activeToken', () => {
|
||||||
DEFAULT_MILESTONES_GRAPHQL.forEach(({ value, title }) => {
|
DEFAULT_MILESTONES.forEach(({ value, title }) => {
|
||||||
const activeToken = wrapper.vm.getActiveMilestone([], value);
|
const activeToken = wrapper.vm.getActiveMilestone([], value);
|
||||||
|
|
||||||
expect(activeToken.title).toEqual(title);
|
expect(activeToken.title).toEqual(title);
|
||||||
|
|
629
spec/requests/api/ci/job_artifacts_spec.rb
Normal file
629
spec/requests/api/ci/job_artifacts_spec.rb
Normal file
|
@ -0,0 +1,629 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe API::Ci::JobArtifacts do
|
||||||
|
include HttpBasicAuthHelpers
|
||||||
|
include DependencyProxyHelpers
|
||||||
|
|
||||||
|
include HttpIOHelpers
|
||||||
|
|
||||||
|
let_it_be(:project, reload: true) do
|
||||||
|
create(:project, :repository, public_builds: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:pipeline, reload: true) do
|
||||||
|
create(:ci_pipeline, project: project,
|
||||||
|
sha: project.commit.id,
|
||||||
|
ref: project.default_branch)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:api_user) { user }
|
||||||
|
let(:reporter) { create(:project_member, :reporter, project: project).user }
|
||||||
|
let(:guest) { create(:project_member, :guest, project: project).user }
|
||||||
|
|
||||||
|
let!(:job) do
|
||||||
|
create(:ci_build, :success, :tags, pipeline: pipeline,
|
||||||
|
artifacts_expire_at: 1.day.since)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'returns unauthorized' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do
|
||||||
|
let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
it 'does not delete artifacts' do
|
||||||
|
expect(job.job_artifacts.size).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns status 401 (unauthorized)' do
|
||||||
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with developer' do
|
||||||
|
it 'does not delete artifacts' do
|
||||||
|
expect(job.job_artifacts.size).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns status 403 (forbidden)' do
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with authorized user' do
|
||||||
|
let(:maintainer) { create(:project_member, :maintainer, project: project).user }
|
||||||
|
let!(:api_user) { maintainer }
|
||||||
|
|
||||||
|
it 'deletes artifacts' do
|
||||||
|
expect(job.job_artifacts.size).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns status 204 (no content)' do
|
||||||
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
|
||||||
|
context 'when job has artifacts' do
|
||||||
|
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
||||||
|
|
||||||
|
let(:artifact) do
|
||||||
|
'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
context 'when project is public' do
|
||||||
|
it 'allows to access artifacts' do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.update_column(:public_builds, true)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public with artifacts that are non public' do
|
||||||
|
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it 'rejects access to artifacts' do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.update_column(:public_builds, true)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the non_public_artifacts feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(non_public_artifacts: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows access to artifacts' do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.update_column(:public_builds, true)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public with builds access disabled' do
|
||||||
|
it 'rejects access to artifacts' do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.update_column(:public_builds, false)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
it 'rejects access and hides existence of artifacts' do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
project.update_column(:public_builds, true)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is authorized' do
|
||||||
|
it 'returns a specific artifact file for a valid path' do
|
||||||
|
expect(Gitlab::Workhorse)
|
||||||
|
.to receive(:send_artifacts_entry)
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.to include('Content-Type' => 'application/json',
|
||||||
|
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
expect(response.parsed_body).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when artifacts are locked' do
|
||||||
|
it 'allows access to expired artifact' do
|
||||||
|
pipeline.artifacts_locked!
|
||||||
|
job.update!(artifacts_expire_at: Time.now - 7.days)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job does not have artifacts' do
|
||||||
|
it 'does not return job artifact file' do
|
||||||
|
get_artifact_file('some/artifact')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_artifact_file(artifact_path)
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/" \
|
||||||
|
"artifacts/#{artifact_path}", api_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /projects/:id/jobs/:job_id/artifacts' do
|
||||||
|
shared_examples 'downloads artifact' do
|
||||||
|
let(:download_headers) do
|
||||||
|
{ 'Content-Transfer-Encoding' => 'binary',
|
||||||
|
'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns specific job artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.headers.to_h).to include(download_headers)
|
||||||
|
expect(response.body).to match_file(job.artifacts_file.file.file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'normal authentication' do
|
||||||
|
context 'job with artifacts' do
|
||||||
|
context 'when artifacts are stored locally' do
|
||||||
|
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'authorized user' do
|
||||||
|
it_behaves_like 'downloads artifact'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unauthorized user' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
it 'does not return specific job artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when artifacts are stored remotely' do
|
||||||
|
let(:proxy_download) { false }
|
||||||
|
let(:job) { create(:ci_build, pipeline: pipeline) }
|
||||||
|
let(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_artifacts_object_storage(proxy_download: proxy_download)
|
||||||
|
|
||||||
|
artifact
|
||||||
|
job.reload
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when proxy download is enabled' do
|
||||||
|
let(:proxy_download) { true }
|
||||||
|
|
||||||
|
it 'responds with the workhorse send-url' do
|
||||||
|
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when proxy download is disabled' do
|
||||||
|
it 'returns location redirect' do
|
||||||
|
expect(response).to have_gitlab_http_status(:found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'authorized user' do
|
||||||
|
it 'returns the file remote URL' do
|
||||||
|
expect(response).to redirect_to(artifact.file.url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'unauthorized user' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
it 'does not return specific job artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when public project guest and artifacts are non public' do
|
||||||
|
let(:api_user) { guest }
|
||||||
|
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.update_column(:visibility_level,
|
||||||
|
Gitlab::VisibilityLevel::PUBLIC)
|
||||||
|
project.update_column(:public_builds, true)
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects access and hides existence of artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the non_public_artifacts feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(non_public_artifacts: false)
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows access to artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not return job artifacts if not uploaded' do
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
|
||||||
|
let(:api_user) { reporter }
|
||||||
|
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_artifacts_object_storage
|
||||||
|
job.success
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_for_ref(ref = pipeline.ref, job_name = job.name)
|
||||||
|
get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), params: { job: job_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not logged in' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get_for_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not find a resource in a private project' do
|
||||||
|
expect(project).to be_private
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logging as guest' do
|
||||||
|
let(:api_user) { guest }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get_for_ref
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gives 403' do
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'non-existing job' do
|
||||||
|
shared_examples 'not found' do
|
||||||
|
it { expect(response).to have_gitlab_http_status(:not_found) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'has no such ref' do
|
||||||
|
before do
|
||||||
|
get_for_ref('TAIL')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'not found'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'has no such job' do
|
||||||
|
before do
|
||||||
|
get_for_ref(pipeline.ref, 'NOBUILD')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'not found'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'find proper job' do
|
||||||
|
let(:job_with_artifacts) { job }
|
||||||
|
|
||||||
|
shared_examples 'a valid file' do
|
||||||
|
context 'when artifacts are stored locally', :sidekiq_might_not_need_inline do
|
||||||
|
let(:download_headers) do
|
||||||
|
{ 'Content-Transfer-Encoding' => 'binary',
|
||||||
|
'Content-Disposition' =>
|
||||||
|
%Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(response).to have_gitlab_http_status(:ok) }
|
||||||
|
it { expect(response.headers.to_h).to include(download_headers) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when artifacts are stored remotely' do
|
||||||
|
let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
|
||||||
|
let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
job.reload
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns location redirect' do
|
||||||
|
expect(response).to have_gitlab_http_status(:found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with regular branch' do
|
||||||
|
before do
|
||||||
|
pipeline.reload
|
||||||
|
pipeline.update!(ref: 'master',
|
||||||
|
sha: project.commit('master').sha)
|
||||||
|
|
||||||
|
get_for_ref('master')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a valid file'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with branch name containing slash' do
|
||||||
|
before do
|
||||||
|
pipeline.reload
|
||||||
|
pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
|
||||||
|
get_for_ref('improve/awesome')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a valid file'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with job name in a child pipeline' do
|
||||||
|
let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
|
||||||
|
let!(:child_job) { create(:ci_build, :artifacts, :success, name: 'rspec', pipeline: child_pipeline) }
|
||||||
|
let(:job_with_artifacts) { child_job }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get_for_ref('master', child_job.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a valid file'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do
|
||||||
|
context 'when job has artifacts' do
|
||||||
|
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
||||||
|
let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
||||||
|
let(:public_builds) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_artifacts_object_storage
|
||||||
|
job.success
|
||||||
|
|
||||||
|
project.update!(visibility_level: visibility_level,
|
||||||
|
public_builds: public_builds)
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:api_user) { nil }
|
||||||
|
|
||||||
|
context 'when project is public' do
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
||||||
|
let(:public_builds) { true }
|
||||||
|
|
||||||
|
it 'allows to access artifacts', :sidekiq_might_not_need_inline do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.to include('Content-Type' => 'application/json',
|
||||||
|
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public with builds access disabled' do
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
||||||
|
let(:public_builds) { false }
|
||||||
|
|
||||||
|
it 'rejects access to artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
expect(json_response).to have_key('message')
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public with non public artifacts' do
|
||||||
|
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) }
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
||||||
|
let(:public_builds) { true }
|
||||||
|
|
||||||
|
it 'rejects access and hides existence of artifacts', :sidekiq_might_not_need_inline do
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
expect(json_response).to have_key('message')
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the non_public_artifacts feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(non_public_artifacts: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows access to artifacts', :sidekiq_might_not_need_inline do
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
|
||||||
|
let(:public_builds) { true }
|
||||||
|
|
||||||
|
it 'rejects access and hides existence of artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(json_response).to have_key('message')
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is authorized' do
|
||||||
|
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
|
||||||
|
let(:public_builds) { true }
|
||||||
|
|
||||||
|
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
|
||||||
|
expect(Gitlab::Workhorse)
|
||||||
|
.to receive(:send_artifacts_entry)
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
|
get_artifact_file(artifact)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.to include('Content-Type' => 'application/json',
|
||||||
|
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
expect(response.parsed_body).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with branch name containing slash' do
|
||||||
|
before do
|
||||||
|
pipeline.reload
|
||||||
|
pipeline.update!(ref: 'improve/awesome',
|
||||||
|
sha: project.commit('improve/awesome').sha)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
|
||||||
|
get_artifact_file(artifact, 'improve/awesome')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.headers.to_h)
|
||||||
|
.to include('Content-Type' => 'application/json',
|
||||||
|
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'non-existing job' do
|
||||||
|
shared_examples 'not found' do
|
||||||
|
it { expect(response).to have_gitlab_http_status(:not_found) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'has no such ref' do
|
||||||
|
before do
|
||||||
|
get_artifact_file('some/artifact', 'wrong-ref')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'not found'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'has no such job' do
|
||||||
|
before do
|
||||||
|
get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'not found'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job does not have artifacts' do
|
||||||
|
let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
|
||||||
|
|
||||||
|
it 'does not return job artifact file' do
|
||||||
|
get_artifact_file('some/artifact')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name)
|
||||||
|
get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), params: { job: job_name }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do
|
||||||
|
before do
|
||||||
|
post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'artifacts did not expire' do
|
||||||
|
let(:job) do
|
||||||
|
create(:ci_build, :trace_artifact, :artifacts, :success,
|
||||||
|
project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps artifacts' do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(job.reload.artifacts_expire_at).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'no artifacts' do
|
||||||
|
let(:job) { create(:ci_build, project: project, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it 'responds with not found' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -445,569 +445,6 @@ RSpec.describe API::Ci::Jobs do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do
|
|
||||||
let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is anonymous' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
it 'does not delete artifacts' do
|
|
||||||
expect(job.job_artifacts.size).to eq 2
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns status 401 (unauthorized)' do
|
|
||||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with developer' do
|
|
||||||
it 'does not delete artifacts' do
|
|
||||||
expect(job.job_artifacts.size).to eq 2
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns status 403 (forbidden)' do
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with authorized user' do
|
|
||||||
let(:maintainer) { create(:project_member, :maintainer, project: project).user }
|
|
||||||
let!(:api_user) { maintainer }
|
|
||||||
|
|
||||||
it 'deletes artifacts' do
|
|
||||||
expect(job.job_artifacts.size).to eq 0
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns status 204 (no content)' do
|
|
||||||
expect(response).to have_gitlab_http_status(:no_content)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
|
|
||||||
context 'when job has artifacts' do
|
|
||||||
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
|
||||||
|
|
||||||
let(:artifact) do
|
|
||||||
'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is anonymous' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
context 'when project is public' do
|
|
||||||
it 'allows to access artifacts' do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PUBLIC)
|
|
||||||
project.update_column(:public_builds, true)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is public with artifacts that are non public' do
|
|
||||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
|
||||||
|
|
||||||
it 'rejects access to artifacts' do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PUBLIC)
|
|
||||||
project.update_column(:public_builds, true)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with the non_public_artifacts feature flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(non_public_artifacts: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows access to artifacts' do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PUBLIC)
|
|
||||||
project.update_column(:public_builds, true)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is public with builds access disabled' do
|
|
||||||
it 'rejects access to artifacts' do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PUBLIC)
|
|
||||||
project.update_column(:public_builds, false)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is private' do
|
|
||||||
it 'rejects access and hides existence of artifacts' do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PRIVATE)
|
|
||||||
project.update_column(:public_builds, true)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is authorized' do
|
|
||||||
it 'returns a specific artifact file for a valid path' do
|
|
||||||
expect(Gitlab::Workhorse)
|
|
||||||
.to receive(:send_artifacts_entry)
|
|
||||||
.and_call_original
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.to include('Content-Type' => 'application/json',
|
|
||||||
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
expect(response.parsed_body).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when artifacts are locked' do
|
|
||||||
it 'allows access to expired artifact' do
|
|
||||||
pipeline.artifacts_locked!
|
|
||||||
job.update!(artifacts_expire_at: Time.now - 7.days)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when job does not have artifacts' do
|
|
||||||
it 'does not return job artifact file' do
|
|
||||||
get_artifact_file('some/artifact')
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_artifact_file(artifact_path)
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/" \
|
|
||||||
"artifacts/#{artifact_path}", api_user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /projects/:id/jobs/:job_id/artifacts' do
|
|
||||||
shared_examples 'downloads artifact' do
|
|
||||||
let(:download_headers) do
|
|
||||||
{ 'Content-Transfer-Encoding' => 'binary',
|
|
||||||
'Content-Disposition' => %q(attachment; filename="ci_build_artifacts.zip"; filename*=UTF-8''ci_build_artifacts.zip) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns specific job artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(response.headers.to_h).to include(download_headers)
|
|
||||||
expect(response.body).to match_file(job.artifacts_file.file.file)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'normal authentication' do
|
|
||||||
context 'job with artifacts' do
|
|
||||||
context 'when artifacts are stored locally' do
|
|
||||||
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'authorized user' do
|
|
||||||
it_behaves_like 'downloads artifact'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'unauthorized user' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
it 'does not return specific job artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when artifacts are stored remotely' do
|
|
||||||
let(:proxy_download) { false }
|
|
||||||
let(:job) { create(:ci_build, pipeline: pipeline) }
|
|
||||||
let(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_artifacts_object_storage(proxy_download: proxy_download)
|
|
||||||
|
|
||||||
artifact
|
|
||||||
job.reload
|
|
||||||
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when proxy download is enabled' do
|
|
||||||
let(:proxy_download) { true }
|
|
||||||
|
|
||||||
it 'responds with the workhorse send-url' do
|
|
||||||
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when proxy download is disabled' do
|
|
||||||
it 'returns location redirect' do
|
|
||||||
expect(response).to have_gitlab_http_status(:found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'authorized user' do
|
|
||||||
it 'returns the file remote URL' do
|
|
||||||
expect(response).to redirect_to(artifact.file.url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'unauthorized user' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
it 'does not return specific job artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when public project guest and artifacts are non public' do
|
|
||||||
let(:api_user) { guest }
|
|
||||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.update_column(:visibility_level,
|
|
||||||
Gitlab::VisibilityLevel::PUBLIC)
|
|
||||||
project.update_column(:public_builds, true)
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'rejects access and hides existence of artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with the non_public_artifacts feature flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(non_public_artifacts: false)
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows access to artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not return job artifacts if not uploaded' do
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
|
|
||||||
let(:api_user) { reporter }
|
|
||||||
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_artifacts_object_storage
|
|
||||||
job.success
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_for_ref(ref = pipeline.ref, job_name = job.name)
|
|
||||||
get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), params: { job: job_name }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when not logged in' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
before do
|
|
||||||
get_for_ref
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not find a resource in a private project' do
|
|
||||||
expect(project).to be_private
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when logging as guest' do
|
|
||||||
let(:api_user) { guest }
|
|
||||||
|
|
||||||
before do
|
|
||||||
get_for_ref
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'gives 403' do
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'non-existing job' do
|
|
||||||
shared_examples 'not found' do
|
|
||||||
it { expect(response).to have_gitlab_http_status(:not_found) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'has no such ref' do
|
|
||||||
before do
|
|
||||||
get_for_ref('TAIL')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'not found'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'has no such job' do
|
|
||||||
before do
|
|
||||||
get_for_ref(pipeline.ref, 'NOBUILD')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'not found'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'find proper job' do
|
|
||||||
let(:job_with_artifacts) { job }
|
|
||||||
|
|
||||||
shared_examples 'a valid file' do
|
|
||||||
context 'when artifacts are stored locally', :sidekiq_might_not_need_inline do
|
|
||||||
let(:download_headers) do
|
|
||||||
{ 'Content-Transfer-Encoding' => 'binary',
|
|
||||||
'Content-Disposition' =>
|
|
||||||
%Q(attachment; filename="#{job_with_artifacts.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}) }
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(response).to have_gitlab_http_status(:ok) }
|
|
||||||
it { expect(response.headers.to_h).to include(download_headers) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when artifacts are stored remotely' do
|
|
||||||
let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
|
|
||||||
let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
job.reload
|
|
||||||
|
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns location redirect' do
|
|
||||||
expect(response).to have_gitlab_http_status(:found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with regular branch' do
|
|
||||||
before do
|
|
||||||
pipeline.reload
|
|
||||||
pipeline.update!(ref: 'master',
|
|
||||||
sha: project.commit('master').sha)
|
|
||||||
|
|
||||||
get_for_ref('master')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'a valid file'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with branch name containing slash' do
|
|
||||||
before do
|
|
||||||
pipeline.reload
|
|
||||||
pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
|
|
||||||
get_for_ref('improve/awesome')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'a valid file'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with job name in a child pipeline' do
|
|
||||||
let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
|
|
||||||
let!(:child_job) { create(:ci_build, :artifacts, :success, name: 'rspec', pipeline: child_pipeline) }
|
|
||||||
let(:job_with_artifacts) { child_job }
|
|
||||||
|
|
||||||
before do
|
|
||||||
get_for_ref('master', child_job.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'a valid file'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name' do
|
|
||||||
context 'when job has artifacts' do
|
|
||||||
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
|
|
||||||
let(:artifact) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
|
||||||
let(:public_builds) { true }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_artifacts_object_storage
|
|
||||||
job.success
|
|
||||||
|
|
||||||
project.update!(visibility_level: visibility_level,
|
|
||||||
public_builds: public_builds)
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is anonymous' do
|
|
||||||
let(:api_user) { nil }
|
|
||||||
|
|
||||||
context 'when project is public' do
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
|
||||||
let(:public_builds) { true }
|
|
||||||
|
|
||||||
it 'allows to access artifacts', :sidekiq_might_not_need_inline do
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.to include('Content-Type' => 'application/json',
|
|
||||||
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is public with builds access disabled' do
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
|
||||||
let(:public_builds) { false }
|
|
||||||
|
|
||||||
it 'rejects access to artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
expect(json_response).to have_key('message')
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is public with non public artifacts' do
|
|
||||||
let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) }
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
|
|
||||||
let(:public_builds) { true }
|
|
||||||
|
|
||||||
it 'rejects access and hides existence of artifacts', :sidekiq_might_not_need_inline do
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
|
||||||
expect(json_response).to have_key('message')
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with the non_public_artifacts feature flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(non_public_artifacts: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows access to artifacts', :sidekiq_might_not_need_inline do
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project is private' do
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
|
|
||||||
let(:public_builds) { true }
|
|
||||||
|
|
||||||
it 'rejects access and hides existence of artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
expect(json_response).to have_key('message')
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is authorized' do
|
|
||||||
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
|
|
||||||
let(:public_builds) { true }
|
|
||||||
|
|
||||||
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
|
|
||||||
expect(Gitlab::Workhorse)
|
|
||||||
.to receive(:send_artifacts_entry)
|
|
||||||
.and_call_original
|
|
||||||
|
|
||||||
get_artifact_file(artifact)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.to include('Content-Type' => 'application/json',
|
|
||||||
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
expect(response.parsed_body).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with branch name containing slash' do
|
|
||||||
before do
|
|
||||||
pipeline.reload
|
|
||||||
pipeline.update!(ref: 'improve/awesome',
|
|
||||||
sha: project.commit('improve/awesome').sha)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
|
|
||||||
get_artifact_file(artifact, 'improve/awesome')
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(response.headers.to_h)
|
|
||||||
.to include('Content-Type' => 'application/json',
|
|
||||||
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'non-existing job' do
|
|
||||||
shared_examples 'not found' do
|
|
||||||
it { expect(response).to have_gitlab_http_status(:not_found) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'has no such ref' do
|
|
||||||
before do
|
|
||||||
get_artifact_file('some/artifact', 'wrong-ref')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'not found'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'has no such job' do
|
|
||||||
before do
|
|
||||||
get_artifact_file('some/artifact', pipeline.ref, 'wrong-job-name')
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'not found'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when job does not have artifacts' do
|
|
||||||
let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
|
|
||||||
|
|
||||||
it 'does not return job artifact file' do
|
|
||||||
get_artifact_file('some/artifact')
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_artifact_file(artifact_path, ref = pipeline.ref, job_name = job.name)
|
|
||||||
get api("/projects/#{project.id}/jobs/artifacts/#{ref}/raw/#{artifact_path}", api_user), params: { job: job_name }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /projects/:id/jobs/:job_id/trace' do
|
describe 'GET /projects/:id/jobs/:job_id/trace' do
|
||||||
before do
|
before do
|
||||||
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
|
get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
|
||||||
|
@ -1249,32 +686,6 @@ RSpec.describe API::Ci::Jobs do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do
|
|
||||||
before do
|
|
||||||
post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'artifacts did not expire' do
|
|
||||||
let(:job) do
|
|
||||||
create(:ci_build, :trace_artifact, :artifacts, :success,
|
|
||||||
project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps artifacts' do
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(job.reload.artifacts_expire_at).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'no artifacts' do
|
|
||||||
let(:job) { create(:ci_build, project: project, pipeline: pipeline) }
|
|
||||||
|
|
||||||
it 'responds with not found' do
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST /projects/:id/jobs/:job_id/play' do
|
describe 'POST /projects/:id/jobs/:job_id/play' do
|
||||||
before do
|
before do
|
||||||
post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user)
|
post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user)
|
||||||
|
|
|
@ -3,33 +3,39 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Ci::JobArtifacts::DestroyBatchService do
|
RSpec.describe Ci::JobArtifacts::DestroyBatchService do
|
||||||
let(:artifacts) { Ci::JobArtifact.all }
|
let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id]) }
|
||||||
let(:service) { described_class.new(artifacts, pick_up_at: Time.current) }
|
let(:service) { described_class.new(artifacts, pick_up_at: Time.current) }
|
||||||
|
|
||||||
|
let_it_be(:artifact_with_file, refind: true) do
|
||||||
|
create(:ci_job_artifact, :zip)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:artifact_without_file, refind: true) do
|
||||||
|
create(:ci_job_artifact)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:undeleted_artifact, refind: true) do
|
||||||
|
create(:ci_job_artifact)
|
||||||
|
end
|
||||||
|
|
||||||
describe '.execute' do
|
describe '.execute' do
|
||||||
subject(:execute) { service.execute }
|
subject(:execute) { service.execute }
|
||||||
|
|
||||||
let_it_be(:artifact, refind: true) do
|
it 'creates a deleted object for artifact with attached file' do
|
||||||
create(:ci_job_artifact)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the artifact has a file attached to it' do
|
|
||||||
before do
|
|
||||||
artifact.file = fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
|
|
||||||
artifact.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a deleted object' do
|
|
||||||
expect { subject }.to change { Ci::DeletedObject.count }.by(1)
|
expect { subject }.to change { Ci::DeletedObject.count }.by(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove the files' do
|
it 'does not remove the attached file' do
|
||||||
expect { execute }.not_to change { artifact.file.exists? }
|
expect { execute }.not_to change { artifact_with_file.file.exists? }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the artifact records' do
|
||||||
|
expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'reports metrics for destroyed artifacts' do
|
it 'reports metrics for destroyed artifacts' do
|
||||||
expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics|
|
expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics|
|
||||||
expect(metrics).to receive(:increment_destroyed_artifacts_count).with(1).and_call_original
|
expect(metrics).to receive(:increment_destroyed_artifacts_count).with(2).and_call_original
|
||||||
expect(metrics).to receive(:increment_destroyed_artifacts_bytes).with(107464).and_call_original
|
expect(metrics).to receive(:increment_destroyed_artifacts_bytes).with(107464).and_call_original
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,7 +45,10 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
|
||||||
context 'ProjectStatistics' do
|
context 'ProjectStatistics' do
|
||||||
it 'resets project statistics' do
|
it 'resets project statistics' do
|
||||||
expect(ProjectStatistics).to receive(:increment_statistic).once
|
expect(ProjectStatistics).to receive(:increment_statistic).once
|
||||||
.with(artifact.project, :build_artifacts_size, -artifact.file.size)
|
.with(artifact_with_file.project, :build_artifacts_size, -artifact_with_file.file.size)
|
||||||
|
.and_call_original
|
||||||
|
expect(ProjectStatistics).to receive(:increment_statistic).once
|
||||||
|
.with(artifact_without_file.project, :build_artifacts_size, 0)
|
||||||
.and_call_original
|
.and_call_original
|
||||||
|
|
||||||
execute
|
execute
|
||||||
|
@ -53,9 +62,15 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns size statistics' do
|
it 'returns size statistics' do
|
||||||
|
expected_updates = {
|
||||||
|
statistics_updates: {
|
||||||
|
artifact_with_file.project => -artifact_with_file.file.size,
|
||||||
|
artifact_without_file.project => 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expect(service.execute(update_stats: false)).to match(
|
expect(service.execute(update_stats: false)).to match(
|
||||||
a_hash_including(statistics_updates: { artifact.project => -artifact.file.size }))
|
a_hash_including(expected_updates))
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -71,7 +86,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
|
||||||
|
|
||||||
it 'raises an exception and stop destroying' do
|
it 'raises an exception and stop destroying' do
|
||||||
expect { execute }.to raise_error(ActiveRecord::RecordNotDestroyed)
|
expect { execute }.to raise_error(ActiveRecord::RecordNotDestroyed)
|
||||||
.and not_change { Ci::JobArtifact.count }.from(1)
|
.and not_change { Ci::JobArtifact.count }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -792,7 +792,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when group has pending builds' do
|
context 'when group has pending builds', :sidekiq_inline do
|
||||||
let_it_be(:project) { create(:project, :public, namespace: group.reload) }
|
let_it_be(:project) { create(:project, :public, namespace: group.reload) }
|
||||||
let_it_be(:other_project) { create(:project) }
|
let_it_be(:other_project) { create(:project) }
|
||||||
let_it_be(:pending_build) { create(:ci_pending_build, project: project) }
|
let_it_be(:pending_build) { create(:ci_pending_build, project: project) }
|
||||||
|
@ -814,6 +814,20 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
|
expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
|
||||||
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
|
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ci_pending_builds_async_update is disabled' do
|
||||||
|
let(:update_pending_build_service) { instance_double(::Ci::PendingBuilds::UpdateGroupWorker) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_pending_builds_async_update: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call the new worker' do
|
||||||
|
expect(::Ci::PendingBuilds::UpdateGroupWorker).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
transfer_service.execute(new_parent_group)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ RSpec.describe Projects::TransferService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when project has pending builds' do
|
context 'when project has pending builds', :sidekiq_inline do
|
||||||
let!(:other_project) { create(:project) }
|
let!(:other_project) { create(:project) }
|
||||||
let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
|
let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
|
||||||
let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }
|
let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }
|
||||||
|
@ -189,6 +189,20 @@ RSpec.describe Projects::TransferService do
|
||||||
expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
|
expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
|
||||||
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
|
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ci_pending_builds_async_update is disabled' do
|
||||||
|
let(:update_pending_build_service) { instance_double(::Ci::PendingBuilds::UpdateProjectWorker) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_pending_builds_async_update: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call the new worker' do
|
||||||
|
expect(::Ci::PendingBuilds::UpdateProjectWorker).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
execute_transfer
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -251,7 +265,7 @@ RSpec.describe Projects::TransferService do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when project has pending builds' do
|
context 'when project has pending builds', :sidekiq_inline do
|
||||||
let!(:other_project) { create(:project) }
|
let!(:other_project) { create(:project) }
|
||||||
let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
|
let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
|
||||||
let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }
|
let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }
|
||||||
|
|
|
@ -78,10 +78,8 @@
|
||||||
- "./spec/services/ci/pipeline_bridge_status_service_spec.rb"
|
- "./spec/services/ci/pipeline_bridge_status_service_spec.rb"
|
||||||
- "./spec/services/ci/pipelines/add_job_service_spec.rb"
|
- "./spec/services/ci/pipelines/add_job_service_spec.rb"
|
||||||
- "./spec/services/ci/retry_build_service_spec.rb"
|
- "./spec/services/ci/retry_build_service_spec.rb"
|
||||||
- "./spec/services/groups/transfer_service_spec.rb"
|
|
||||||
- "./spec/services/projects/destroy_service_spec.rb"
|
- "./spec/services/projects/destroy_service_spec.rb"
|
||||||
- "./spec/services/projects/overwrite_project_service_spec.rb"
|
- "./spec/services/projects/overwrite_project_service_spec.rb"
|
||||||
- "./spec/services/projects/transfer_service_spec.rb"
|
|
||||||
- "./spec/services/resource_access_tokens/revoke_service_spec.rb"
|
- "./spec/services/resource_access_tokens/revoke_service_spec.rb"
|
||||||
- "./spec/services/users/destroy_service_spec.rb"
|
- "./spec/services/users/destroy_service_spec.rb"
|
||||||
- "./spec/services/users/reject_service_spec.rb"
|
- "./spec/services/users/reject_service_spec.rb"
|
||||||
|
|
40
spec/workers/ci/pending_builds/update_group_worker_spec.rb
Normal file
40
spec/workers/ci/pending_builds/update_group_worker_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ci::PendingBuilds::UpdateGroupWorker do
|
||||||
|
describe '#perform' do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
context 'when a group is not provided' do
|
||||||
|
it 'does not call the service' do
|
||||||
|
expect(::Ci::UpdatePendingBuildService).not_to receive(:new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when everything is ok' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:update_pending_build_service) { instance_double(::Ci::UpdatePendingBuildService) }
|
||||||
|
let(:update_params) { { "namespace_id" => group.id } }
|
||||||
|
|
||||||
|
it 'calls the service' do
|
||||||
|
expect(::Ci::UpdatePendingBuildService).to receive(:new).with(group, update_params).and_return(update_pending_build_service)
|
||||||
|
expect(update_pending_build_service).to receive(:execute)
|
||||||
|
|
||||||
|
worker.perform(group.id, update_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'an idempotent worker' do
|
||||||
|
let(:pending_build) { create(:ci_pending_build) }
|
||||||
|
let(:update_params) { { "namespace_id" => pending_build.namespace_id } }
|
||||||
|
let(:job_args) { [pending_build.namespace_id, update_params] }
|
||||||
|
|
||||||
|
it 'updates the pending builds' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(pending_build.reload.namespace_id).to eq(update_params["namespace_id"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
spec/workers/ci/pending_builds/update_project_worker_spec.rb
Normal file
40
spec/workers/ci/pending_builds/update_project_worker_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Ci::PendingBuilds::UpdateProjectWorker do
|
||||||
|
describe '#perform' do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
context 'when a project is not provided' do
|
||||||
|
it 'does not call the service' do
|
||||||
|
expect(::Ci::UpdatePendingBuildService).not_to receive(:new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when everything is ok' do
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:update_pending_build_service) { instance_double(::Ci::UpdatePendingBuildService) }
|
||||||
|
let(:update_params) { { "namespace_id" => group.id } }
|
||||||
|
|
||||||
|
it 'calls the service' do
|
||||||
|
expect(::Ci::UpdatePendingBuildService).to receive(:new).with(project, update_params).and_return(update_pending_build_service)
|
||||||
|
expect(update_pending_build_service).to receive(:execute)
|
||||||
|
|
||||||
|
worker.perform(project.id, update_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'an idempotent worker' do
|
||||||
|
let(:pending_build) { create(:ci_pending_build) }
|
||||||
|
let(:job_args) { [pending_build.project_id, update_params] }
|
||||||
|
|
||||||
|
it 'updates the pending builds' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(pending_build.reload.namespace_id).to eq(update_params["namespace_id"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue