Commit 5541e08b authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 977eef18 0f642b99
...@@ -38,7 +38,7 @@ module Security ...@@ -38,7 +38,7 @@ module Security
def execute def execute
return [] if @job_types.empty? return [] if @job_types.empty?
if Feature.enabled?(:ci_build_metadata_config) if Feature.enabled?(:ci_build_metadata_config, pipeline.project, default_enabled: :yaml)
find_jobs find_jobs
else else
find_jobs_legacy find_jobs_legacy
......
...@@ -22,8 +22,8 @@ module Ci ...@@ -22,8 +22,8 @@ module Ci
validates :build, presence: true validates :build, presence: true
validates :secrets, json_schema: { filename: 'build_metadata_secrets' } validates :secrets, json_schema: { filename: 'build_metadata_secrets' }
serialize :config_options, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout chronic_duration_attr_reader :timeout_human_readable, :timeout
......
...@@ -77,7 +77,7 @@ module Ci ...@@ -77,7 +77,7 @@ module Ci
def write_metadata_attribute(legacy_key, metadata_key, value) def write_metadata_attribute(legacy_key, metadata_key, value)
# save to metadata or this model depending on the state of feature flag # save to metadata or this model depending on the state of feature flag
if Feature.enabled?(:ci_build_metadata_config) if Feature.enabled?(:ci_build_metadata_config, project, default_enabled: :yaml)
ensure_metadata.write_attribute(metadata_key, value) ensure_metadata.write_attribute(metadata_key, value)
write_attribute(legacy_key, nil) write_attribute(legacy_key, nil)
else else
......
...@@ -5,4 +5,4 @@ rollout_issue_url: ...@@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '14.0' milestone: '14.0'
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true
--- ---
name: ci_build_metadata_config name: ci_build_metadata_config
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7238 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7238
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330954
milestone: '11.7' milestone: '11.7'
type: development type: development
group: group::pipeline execution group: group::pipeline execution
......
---
name: erase_traces_from_already_archived_jobs_when_archiving_again
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56353
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326679
milestone: "13.11"
type: development
group: group::pipeline execution
default_enabled: true
--- ---
name: sec_secret_detection_ui_enable name: jira_issue_details_edit_status
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57869 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326049 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628
milestone: '13.11' milestone: '14.1'
type: development type: development
group: group::static analysis group: group::ecosystem
default_enabled: true default_enabled: false
--- ---
name: use_traversal_ids_for_ancestors name: use_traversal_ids_for_ancestors
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57137 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57137
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334952
milestone: '13.12' milestone: '13.12'
type: development type: development
group: group::access group: group::access
......
# frozen_string_literal: true
class ScheduleDeleteOrphanedDeployments < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'DeleteOrphanedDeployments'
BATCH_SIZE = 100_000
DELAY_INTERVAL = 2.minutes
disable_ddl_transaction!
def up
queue_background_migration_jobs_by_range_at_intervals(
define_batchable_model('deployments'),
MIGRATION,
DELAY_INTERVAL,
batch_size: BATCH_SIZE,
track_jobs: true
)
end
def down
# no-op
end
end
432954295d6f3a2a45f3deef42b547ffe42501beaea4f376e1be51cf148de671
\ No newline at end of file
...@@ -139,18 +139,8 @@ always take the latest Secret Detection artifact available. ...@@ -139,18 +139,8 @@ always take the latest Secret Detection artifact available.
### Enable Secret Detection via an automatic merge request **(ULTIMATE SELF)** ### Enable Secret Detection via an automatic merge request **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4496) in GitLab 13.11. > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4496) in GitLab 13.11, behind a feature flag, enabled by default.
> - [Deployed behind a feature flag](../../../user/feature_flags.md), enabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/329886) in GitLab 14.1.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-configure-secret-detection-via-a-merge-request). **(ULTIMATE SELF)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
There can be
[risks when disabling released features](../../../user/feature_flags.md#risks-when-disabling-released-features).
Refer to this feature's version history for more details.
To enable Secret Detection in a project, you can create a merge request To enable Secret Detection in a project, you can create a merge request
from the Security Configuration page. from the Security Configuration page.
...@@ -409,22 +399,3 @@ secret_detection: ...@@ -409,22 +399,3 @@ secret_detection:
variables: variables:
GIT_DEPTH: 100 GIT_DEPTH: 100
``` ```
### Enable or disable Configure Secret Detection via a Merge Request
Configure Secret Detection via a Merge Request is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:sec_secret_detection_ui_enable)
```
To disable it:
```ruby
Feature.disable(:sec_secret_detection_ui_enable)
```
...@@ -48,6 +48,10 @@ Iteration cadences automate some common iteration tasks. They can be used to ...@@ -48,6 +48,10 @@ Iteration cadences automate some common iteration tasks. They can be used to
automatically create iterations every 1, 2, 3, 4, or 6 weeks. They can also automatically create iterations every 1, 2, 3, 4, or 6 weeks. They can also
be configured to automatically roll over incomplete issues to the next iteration. be configured to automatically roll over incomplete issues to the next iteration.
With iteration cadences enabled, you must first
[create an iteration cadence](#create-an-iteration-cadence) before you can
[create an iteration](#create-an-iteration).
### Create an iteration cadence ### Create an iteration cadence
Prerequisites: Prerequisites:
...@@ -94,7 +98,7 @@ To create an iteration: ...@@ -94,7 +98,7 @@ To create an iteration:
1. On the top bar, select **Menu > Groups** and find your group. 1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**. 1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Add iteration** for the cadence you want to add to. 1. Select **New iteration**.
1. Enter the title, a description (optional), a start date, and a due date. 1. Enter the title, a description (optional), a start date, and a due date.
1. Select **Create iteration**. The iteration details page opens. 1. Select **Create iteration**. The iteration details page opens.
...@@ -191,13 +195,13 @@ can enable it. ...@@ -191,13 +195,13 @@ can enable it.
To enable it: To enable it:
```ruby ```ruby
Feature.enable(:iterations_cadences) Feature.enable(:iteration_cadences)
``` ```
To disable it: To disable it:
```ruby ```ruby
Feature.disable(:iterations_cadences) Feature.disable(:iteration_cadences)
``` ```
<!-- ## Troubleshooting <!-- ## Troubleshooting
......
...@@ -5,3 +5,22 @@ export const fetchIssue = async (issuePath) => { ...@@ -5,3 +5,22 @@ export const fetchIssue = async (issuePath) => {
return data; return data;
}); });
}; };
export const fetchIssueStatuses = () => {
// We are using mock data here which should come from the backend
return new Promise((resolve) => {
setTimeout(() => {
// eslint-disable-next-line @gitlab/require-i18n-strings
resolve([{ title: 'In Progress' }, { title: 'Done' }]);
}, 1000);
});
};
export const updateIssue = (issue, { status }) => {
// We are using mock call here which should become a backend call
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...issue, status });
}, 1000);
});
};
...@@ -7,9 +7,11 @@ import { ...@@ -7,9 +7,11 @@ import {
GlBadge, GlBadge,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { fetchIssue } from 'ee/integrations/jira/issues_show/api'; import { fetchIssue, fetchIssueStatuses, updateIssue } from 'ee/integrations/jira/issues_show/api';
import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue'; import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { issueStates, issueStateLabels } from 'ee/integrations/jira/issues_show/constants'; import { issueStates, issueStateLabels } from 'ee/integrations/jira/issues_show/constants';
import createFlash from '~/flash';
import IssuableShow from '~/issuable_show/components/issuable_show_root.vue'; import IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -38,8 +40,11 @@ export default { ...@@ -38,8 +40,11 @@ export default {
data() { data() {
return { return {
isLoading: true, isLoading: true,
isLoadingStatus: false,
isUpdatingStatus: false,
errorMessage: null, errorMessage: null,
issue: {}, issue: {},
statuses: [],
}; };
}, },
computed: { computed: {
...@@ -78,6 +83,41 @@ export default { ...@@ -78,6 +83,41 @@ export default {
jiraIssueCommentId(id) { jiraIssueCommentId(id) {
return `jira_note_${id}`; return `jira_note_${id}`;
}, },
onIssueStatusFetch() {
this.isLoadingStatus = true;
fetchIssueStatuses()
.then((response) => {
this.statuses = response;
})
.catch(() => {
createFlash({
message: s__(
'JiraService|Failed to load Jira issue statuses. View the issue in Jira, or reload the page.',
),
});
})
.finally(() => {
this.isLoadingStatus = false;
});
},
onIssueStatusUpdated(status) {
this.isUpdatingStatus = true;
updateIssue(this.issue, { status })
.then(() => {
this.issue = { ...this.issue, status };
})
.catch(() => {
createFlash({
message: s__(
'JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page.',
),
});
})
.finally(() => {
this.isUpdatingStatus = false;
});
},
}, },
}; };
</script> </script>
...@@ -117,7 +157,15 @@ export default { ...@@ -117,7 +157,15 @@ export default {
<template #status-badge>{{ statusBadgeText }}</template> <template #status-badge>{{ statusBadgeText }}</template>
<template #right-sidebar-items="{ sidebarExpanded }"> <template #right-sidebar-items="{ sidebarExpanded }">
<jira-issue-sidebar :sidebar-expanded="sidebarExpanded" :issue="issue" /> <jira-issue-sidebar
:sidebar-expanded="sidebarExpanded"
:issue="issue"
:is-loading-status="isLoadingStatus"
:is-updating-status="isUpdatingStatus"
:statuses="statuses"
@issue-status-fetch="onIssueStatusFetch"
@issue-status-updated="onIssueStatusUpdated"
/>
</template> </template>
<template #discussion> <template #discussion>
......
<script> <script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import IssueFieldDropdown from './issue_field_dropdown.vue';
export default { export default {
directives: { directives: {
...@@ -8,12 +10,50 @@ export default { ...@@ -8,12 +10,50 @@ export default {
}, },
components: { components: {
GlIcon, GlIcon,
IssueFieldDropdown,
SidebarEditableItem,
},
provide() {
return {
isClassicSidebar: true,
canUpdate: this.canUpdate,
};
}, },
props: { props: {
canUpdate: {
type: Boolean,
required: false,
default: false,
},
dropdownEmpty: {
type: String,
required: false,
default: null,
},
dropdownTitle: {
type: String,
required: false,
default: null,
},
icon: { icon: {
type: String, type: String,
required: true, required: true,
}, },
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
updating: {
type: Boolean,
required: false,
default: false,
},
title: { title: {
type: String, type: String,
required: true, required: true,
...@@ -44,20 +84,58 @@ export default { ...@@ -44,20 +84,58 @@ export default {
i18n: { i18n: {
none: __('None'), none: __('None'),
}, },
methods: {
showDropdown() {
this.$refs.dropdown.showDropdown();
this.$emit('issue-field-fetch');
},
expandSidebarAndOpenDropdown() {
this.$emit('expand-sidebar', this.$refs.editableItem);
},
onIssueFieldUpdated(value) {
this.$emit('issue-field-updated', value);
},
},
}; };
</script> </script>
<template> <template>
<div class="block"> <div class="block">
<div v-gl-tooltip="tooltipProps" class="sidebar-collapsed-icon" data-testid="field-collapsed"> <sidebar-editable-item
<gl-icon :name="icon" /> ref="editableItem"
</div> :loading="updating"
:title="title"
@open="showDropdown"
>
<template #collapsed>
<div
v-gl-tooltip="tooltipProps"
class="sidebar-collapsed-icon"
data-testid="field-collapsed"
@click="expandSidebarAndOpenDropdown"
>
<gl-icon :name="icon" />
</div>
<div class="hide-collapsed">
<div class="value" data-testid="field-value">
<span :class="valueClass">{{ valueWithFallback }}</span>
</div>
</div>
</template>
<div class="hide-collapsed"> <template #default>
<div class="title" data-testid="field-title">{{ title }}</div> <issue-field-dropdown
<div class="value"> v-if="canUpdate"
<span :class="valueClass" data-testid="field-value">{{ valueWithFallback }}</span> ref="dropdown"
</div> :empty-text="dropdownEmpty"
</div> :items="items"
:loading="loading"
:text="valueWithFallback"
:title="dropdownTitle"
@issue-field-updated="onIssueFieldUpdated"
/>
</template>
</sidebar-editable-item>
</div> </div>
</template> </template>
<script>
import { GlDropdown, GlDropdownItem, GlDropdownText, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
},
props: {
emptyText: {
type: String,
required: false,
default: null,
},
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: true,
},
text: {
type: String,
required: false,
default: null,
},
title: {
type: String,
required: false,
default: null,
},
},
computed: {
noItems() {
return this.items.length === 0;
},
},
methods: {
showDropdown() {
this.$refs.dropdown.show();
},
selectItem(item) {
this.$emit('issue-field-updated', item.title);
},
},
};
</script>
<template>
<gl-dropdown ref="dropdown" :text="text" :header-text="title" block lazy>
<div v-if="loading" class="gl-h-13">
<gl-loading-icon size="md" />
</div>
<div v-else>
<gl-dropdown-text v-if="noItems">{{ emptyText }}</gl-dropdown-text>
<gl-dropdown-item v-for="item in items" :key="item.title" @click="selectItem(item)">
{{ item.title }}
</gl-dropdown-item>
</div>
</gl-dropdown>
</template>
<script> <script>
import { labelsFilterParam } from 'ee/integrations/jira/issues_show/constants'; import { labelsFilterParam } from 'ee/integrations/jira/issues_show/constants';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue'; import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Assignee from './assignee.vue'; import Assignee from './assignee.vue';
import IssueDueDate from './issue_due_date.vue'; import IssueDueDate from './issue_due_date.vue';
import IssueField from './issue_field.vue'; import IssueField from './issue_field.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
CopyableField, CopyableField,
LabelsSelect, LabelsSelect,
}, },
mixins: [glFeatureFlagsMixin()],
inject: { inject: {
issuesListPath: { issuesListPath: {
default: null, default: null,
...@@ -30,6 +32,21 @@ export default { ...@@ -30,6 +32,21 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isLoadingStatus: {
type: Boolean,
required: false,
default: false,
},
isUpdatingStatus: {
type: Boolean,
required: false,
default: false,
},
statuses: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
assignee() { assignee() {
...@@ -39,12 +56,50 @@ export default { ...@@ -39,12 +56,50 @@ export default {
reference() { reference() {
return this.issue.references?.relative; return this.issue.references?.relative;
}, },
canUpdateStatus() {
return this.glFeatures.jiraIssueDetailsEditStatus;
},
}, },
labelsFilterParam, labelsFilterParam,
i18n: { i18n: {
statusTitle: __('Status'), statusTitle: __('Status'),
statusDropdownEmpty: s__('JiraService|No available statuses'),
statusDropdownTitle: __('Change status'),
referenceName: __('Reference'), referenceName: __('Reference'),
}, },
mounted() {
this.sidebarEl = document.querySelector('aside.right-sidebar');
this.sidebarToggleEl = document.querySelector('.js-toggle-right-sidebar-button');
},
methods: {
toggleSidebar() {
this.sidebarToggleEl.dispatchEvent(new Event('click'));
},
expandSidebarAndOpenDropdown(dropdownRef = null) {
// Expand the sidebar if not already expanded.
if (!this.sidebarExpanded) {
this.toggleSidebar();
}
if (dropdownRef) {
// Wait for sidebar expand animation to complete
// before revealing the dropdown.
this.sidebarEl.addEventListener(
'transitionend',
() => {
dropdownRef.expand();
},
{ once: true },
);
}
},
onIssueStatusFetch() {
this.$emit('issue-status-fetch');
},
onIssueStatusUpdated(status) {
this.$emit('issue-status-updated', status);
},
},
}; };
</script> </script>
...@@ -52,7 +107,20 @@ export default { ...@@ -52,7 +107,20 @@ export default {
<div> <div>
<assignee class="block" :assignee="assignee" /> <assignee class="block" :assignee="assignee" />
<issue-due-date :due-date="issue.dueDate" /> <issue-due-date :due-date="issue.dueDate" />
<issue-field icon="progress" :title="$options.i18n.statusTitle" :value="issue.status" /> <issue-field
icon="progress"
:can-update="canUpdateStatus"
:dropdown-title="$options.i18n.statusDropdownTitle"
:dropdown-empty="$options.i18n.statusDropdownEmpty"
:items="statuses"
:loading="isLoadingStatus"
:title="$options.i18n.statusTitle"
:updating="isUpdatingStatus"
:value="issue.status"
@expand-sidebar="expandSidebarAndOpenDropdown"
@issue-field-fetch="onIssueStatusFetch"
@issue-field-updated="onIssueStatusUpdated"
/>
<labels-select <labels-select
:selected-labels="issue.labels" :selected-labels="issue.labels"
:labels-filter-base-path="issuesListPath" :labels-filter-base-path="issuesListPath"
......
...@@ -9,6 +9,7 @@ query IterationCadences( ...@@ -9,6 +9,7 @@ query IterationCadences(
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
iterationCadences( iterationCadences(
includeAncestorGroups: true
before: $beforeCursor before: $beforeCursor
after: $afterCursor after: $afterCursor
first: $firstPageSize first: $firstPageSize
......
...@@ -25,9 +25,6 @@ export default { ...@@ -25,9 +25,6 @@ export default {
if (!this.glFeatures.secDependencyScanningUiEnable) { if (!this.glFeatures.secDependencyScanningUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_DEPENDENCY_SCANNING]; delete scannerComponentMapCopy[REPORT_TYPE_DEPENDENCY_SCANNING];
} }
if (!this.glFeatures.secSecretDetectionUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_SECRET_DETECTION];
}
return scannerComponentMapCopy; return scannerComponentMapCopy;
}, },
manageComponent() { manageComponent() {
......
...@@ -15,7 +15,6 @@ module EE ...@@ -15,7 +15,6 @@ module EE
before_action only: [:show] do before_action only: [:show] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false) push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
push_frontend_feature_flag(:sec_dependency_scanning_ui_enable, project, default_enabled: :yaml) push_frontend_feature_flag(:sec_dependency_scanning_ui_enable, project, default_enabled: :yaml)
push_frontend_feature_flag(:sec_secret_detection_ui_enable, project, default_enabled: :yaml)
push_frontend_feature_flag(:dast_configuration_ui, project, default_enabled: :yaml) push_frontend_feature_flag(:dast_configuration_ui, project, default_enabled: :yaml)
end end
......
...@@ -13,6 +13,9 @@ module Projects ...@@ -13,6 +13,9 @@ module Projects
name: 'i_ecosystem_jira_service_list_issues' name: 'i_ecosystem_jira_service_list_issues'
before_action :check_feature_enabled! before_action :check_feature_enabled!
before_action only: :show do
push_frontend_feature_flag(:jira_issue_details_edit_status, project, default_enabled: :yaml)
end
rescue_from ::Projects::Integrations::Jira::IssuesFinder::IntegrationError, with: :render_integration_error rescue_from ::Projects::Integrations::Jira::IssuesFinder::IntegrationError, with: :render_integration_error
rescue_from ::Projects::Integrations::Jira::IssuesFinder::RequestError, with: :render_request_error rescue_from ::Projects::Integrations::Jira::IssuesFinder::RequestError, with: :render_request_error
......
...@@ -11,13 +11,9 @@ module LatestPipelineInformation ...@@ -11,13 +11,9 @@ module LatestPipelineInformation
strong_memoize("latest_builds_reports_#{only_successful_builds}" ) do strong_memoize("latest_builds_reports_#{only_successful_builds}" ) do
builds = latest_security_builds builds = latest_security_builds
builds = builds.select { |build| build.status == 'success' } if only_successful_builds builds = builds.select { |build| build.status == 'success' } if only_successful_builds
builds.map do |build| builds.flat_map do |build|
if Feature.enabled?(:ci_build_metadata_config) build.options[:artifacts][:reports].keys
build.metadata.config_options[:artifacts][:reports].keys.map(&:to_sym) end
else
build.options[:artifacts][:reports].keys
end
end.flatten
end end
end end
......
...@@ -5,19 +5,21 @@ require 'spec_helper' ...@@ -5,19 +5,21 @@ require 'spec_helper'
RSpec.describe 'User views iteration cadences', :js do RSpec.describe 'User views iteration cadences', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:cadence) { create(:iterations_cadence, group: group) } let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:other_cadence) { create(:iterations_cadence, group: group) } let_it_be(:other_cadence) { create(:iterations_cadence, group: group) }
let_it_be(:subgroup_cadence) { create(:iterations_cadence, group: subgroup) }
let_it_be(:iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence) } let_it_be(:iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence) }
let_it_be(:closed_iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence, start_date: 2.weeks.ago, due_date: 1.week.ago) } let_it_be(:closed_iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence, start_date: 2.weeks.ago, due_date: 1.week.ago) }
let_it_be(:iteration_in_other_cadence) { create(:iteration, group: group, iterations_cadence: other_cadence) } let_it_be(:iteration_in_other_cadence) { create(:iteration, group: group, iterations_cadence: other_cadence) }
before do before do
stub_licensed_features(iterations: true) stub_licensed_features(iterations: true)
visit group_iteration_cadences_path(group)
end end
it 'shows iteration cadences with iterations when expanded', :aggregate_failures do it 'shows iteration cadences with iterations when expanded', :aggregate_failures do
visit group_iteration_cadences_path(group)
expect(page).to have_title('Iteration cadences') expect(page).to have_title('Iteration cadences')
expect(page).to have_content(cadence.title) expect(page).to have_content(cadence.title)
expect(page).to have_content(other_cadence.title) expect(page).to have_content(other_cadence.title)
...@@ -27,15 +29,25 @@ RSpec.describe 'User views iteration cadences', :js do ...@@ -27,15 +29,25 @@ RSpec.describe 'User views iteration cadences', :js do
click_button cadence.title click_button cadence.title
expect(page).to have_content(iteration_in_cadence.title) expect(page).to have_content(iteration_in_cadence.title)
expect(page).not_to have_content(subgroup_cadence.title)
expect(page).not_to have_content(iteration_in_other_cadence.title) expect(page).not_to have_content(iteration_in_other_cadence.title)
expect(page).not_to have_content(closed_iteration_in_cadence.title) expect(page).not_to have_content(closed_iteration_in_cadence.title)
end end
it 'only shows completed iterations on Done tab', :aggregate_failures do it 'only shows completed iterations on Done tab', :aggregate_failures do
visit group_iteration_cadences_path(group)
click_link 'Done' click_link 'Done'
click_button cadence.title click_button cadence.title
expect(page).not_to have_content(iteration_in_cadence.title) expect(page).not_to have_content(iteration_in_cadence.title)
expect(page).to have_content(closed_iteration_in_cadence.title) expect(page).to have_content(closed_iteration_in_cadence.title)
end end
it 'shows inherited cadences in subgroup', :aggregate_failures do
visit group_iteration_cadences_path(subgroup)
expect(page).to have_content(cadence.title)
expect(page).to have_content(other_cadence.title)
expect(page).to have_content(subgroup_cadence.title)
end
end end
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import * as JiraIssuesShowApi from 'ee/integrations/jira/issues_show/api';
import JiraIssuesShow from 'ee/integrations/jira/issues_show/components/jira_issues_show_root.vue'; import JiraIssuesShow from 'ee/integrations/jira/issues_show/components/jira_issues_show_root.vue';
import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { issueStates } from 'ee/integrations/jira/issues_show/constants'; import { issueStates } from 'ee/integrations/jira/issues_show/constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue'; import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import IssuableShow from '~/issuable_show/components/issuable_show_root.vue'; import IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mockJiraIssue } from '../mock_data'; import { mockJiraIssue } from '../mock_data';
...@@ -18,14 +22,16 @@ describe('JiraIssuesShow', () => { ...@@ -18,14 +22,16 @@ describe('JiraIssuesShow', () => {
const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findIssuableShow = () => wrapper.findComponent(IssuableShow); const findIssuableShow = () => wrapper.findComponent(IssuableShow);
const findJiraIssueSidebar = () => wrapper.findComponent(JiraIssueSidebar);
const findIssuableShowStatusBadge = () => const findIssuableShowStatusBadge = () =>
wrapper.findComponent(IssuableHeader).find('[data-testid="status"]'); wrapper.findComponent(IssuableHeader).find('[data-testid="status"]');
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(JiraIssuesShow, { wrapper = shallowMount(JiraIssuesShow, {
stubs: { stubs: {
IssuableShow,
IssuableHeader, IssuableHeader,
IssuableShow,
IssuableSidebar,
}, },
provide: { provide: {
issuesShowPath: mockJiraIssuesShowPath, issuesShowPath: mockJiraIssuesShowPath,
...@@ -39,11 +45,7 @@ describe('JiraIssuesShow', () => { ...@@ -39,11 +45,7 @@ describe('JiraIssuesShow', () => {
afterEach(() => { afterEach(() => {
mockAxios.restore(); mockAxios.restore();
wrapper.destroy();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
}); });
describe('when issue is loading', () => { describe('when issue is loading', () => {
...@@ -109,4 +111,45 @@ describe('JiraIssuesShow', () => { ...@@ -109,4 +111,45 @@ describe('JiraIssuesShow', () => {
expect(findIssuableShowStatusBadge().text()).toBe(badgeText); expect(findIssuableShowStatusBadge().text()).toBe(badgeText);
}); });
}); });
describe('JiraIssueSidebar events', () => {
beforeEach(async () => {
mockAxios.onGet(mockJiraIssuesShowPath).replyOnce(200, mockJiraIssue);
createComponent();
await waitForPromises();
});
it('fetches issue statuses on issue-status-fetch', async () => {
const fetchIssueStatusesSpy = jest
.spyOn(JiraIssuesShowApi, 'fetchIssueStatuses')
.mockResolvedValue();
findJiraIssueSidebar().vm.$emit('issue-status-fetch');
await wrapper.vm.$nextTick();
expect(fetchIssueStatusesSpy).toHaveBeenCalled();
expect(findJiraIssueSidebar().props('isLoadingStatus')).toBe(true);
await waitForPromises();
expect(findJiraIssueSidebar().props('isLoadingStatus')).toBe(false);
});
it('updates issue status on issue-status-updated', async () => {
const updateIssueSpy = jest.spyOn(JiraIssuesShowApi, 'updateIssue').mockResolvedValue();
const status = 'In Review';
findJiraIssueSidebar().vm.$emit('issue-status-updated', status);
await wrapper.vm.$nextTick();
expect(updateIssueSpy).toHaveBeenCalledWith(expect.any(Object), { status });
expect(findJiraIssueSidebar().props('isUpdatingStatus')).toBe(true);
await waitForPromises();
expect(findJiraIssueSidebar().props('isUpdatingStatus')).toBe(false);
});
});
}); });
import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssueFieldDropdown from 'ee/integrations/jira/issues_show/components/sidebar/issue_field_dropdown.vue';
import { mockJiraIssueStatuses } from '../../mock_data';
describe('IssueFieldDropdown', () => {
let wrapper;
const emptyText = 'empty text';
const defaultProps = {
emptyText,
text: 'issue field text',
title: 'issue field header text',
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(IssueFieldDropdown, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
const findAllGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
it.each`
loading | items
${true} | ${[]}
${true} | ${mockJiraIssueStatuses}
${false} | ${[]}
${false} | ${mockJiraIssueStatuses}
`('with loading = $loading, items = $items', ({ loading, items }) => {
createComponent({
props: {
loading,
items,
},
});
expect(findGlLoadingIcon().exists()).toBe(loading);
if (!loading) {
if (items.length) {
findAllGlDropdownItems().wrappers.forEach((itemWrapper, index) => {
expect(itemWrapper.text()).toBe(mockJiraIssueStatuses[index].title);
});
} else {
expect(wrapper.text()).toBe(emptyText);
}
}
});
});
import { GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssueField from 'ee/integrations/jira/issues_show/components/sidebar/issue_field.vue'; import IssueField from 'ee/integrations/jira/issues_show/components/sidebar/issue_field.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
describe('IssueField', () => { describe('IssueField', () => {
let wrapper; let wrapper;
...@@ -15,37 +16,44 @@ describe('IssueField', () => { ...@@ -15,37 +16,44 @@ describe('IssueField', () => {
}; };
const createComponent = ({ props = {} } = {}) => { const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper( wrapper = shallowMountExtended(IssueField, {
shallowMount(IssueField, { directives: {
propsData: { ...defaultProps, ...props }, GlTooltip: createMockDirective(),
directives: { },
GlTooltip: createMockDirective(), propsData: { ...defaultProps, ...props },
}, stubs: {
}), SidebarEditableItem,
); },
});
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findFieldTitle = () => wrapper.findByTestId('field-title'); const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findFieldValue = () => wrapper.findByTestId('field-value'); const findEditButton = () => wrapper.findComponent(GlButton);
const findFieldCollapsed = () => wrapper.findByTestId('field-collapsed'); const findFieldCollapsed = () => wrapper.findByTestId('field-collapsed');
const findFieldCollapsedTooltip = () => getBinding(findFieldCollapsed().element, 'gl-tooltip'); const findFieldCollapsedTooltip = () => getBinding(findFieldCollapsed().element, 'gl-tooltip');
const findFieldValue = () => wrapper.findByTestId('field-value');
const findGlIcon = () => wrapper.findComponent(GlIcon); const findGlIcon = () => wrapper.findComponent(GlIcon);
it('renders title', () => { describe('template', () => {
createComponent(); beforeEach(() => {
createComponent();
});
expect(findFieldTitle().text()).toBe(defaultProps.title); it('renders title', () => {
}); expect(findEditableItem().props('title')).toBe(defaultProps.title);
});
it('renders GlIcon (when collapsed)', () => { it('renders GlIcon (when collapsed)', () => {
createComponent(); expect(findGlIcon().props('name')).toBe(defaultProps.icon);
});
expect(findGlIcon().props('name')).toBe(defaultProps.icon); it('does not render "Edit" button', () => {
expect(findEditButton().exists()).toBe(false);
});
}); });
describe('without value prop', () => { describe('without value prop', () => {
...@@ -53,7 +61,7 @@ describe('IssueField', () => { ...@@ -53,7 +61,7 @@ describe('IssueField', () => {
createComponent(); createComponent();
}); });
it('renders fallback value with "no-value" class', () => { it('falls back to "None"', () => {
expect(findFieldValue().text()).toBe('None'); expect(findFieldValue().text()).toBe('None');
}); });
...@@ -74,7 +82,7 @@ describe('IssueField', () => { ...@@ -74,7 +82,7 @@ describe('IssueField', () => {
}); });
}); });
it('renders value', () => { it('renders the value', () => {
expect(findFieldValue().text()).toBe(value); expect(findFieldValue().text()).toBe(value);
}); });
...@@ -85,4 +93,25 @@ describe('IssueField', () => { ...@@ -85,4 +93,25 @@ describe('IssueField', () => {
expect(tooltip.value.title).toBe(value); expect(tooltip.value.title).toBe(value);
}); });
}); });
describe('with canUpdate = true', () => {
beforeEach(() => {
createComponent({
props: { canUpdate: true },
});
});
it('renders "Edit" button', () => {
expect(findEditButton().text()).toBe('Edit');
});
it('emits "issue-field-fetch" when dropdown is opened', () => {
wrapper.vm.$refs.dropdown.showDropdown = jest.fn();
findEditableItem().vm.$emit('open');
expect(wrapper.vm.$refs.dropdown.showDropdown).toHaveBeenCalled();
expect(wrapper.emitted('issue-field-fetch')).toHaveLength(1);
});
});
}); });
...@@ -25,10 +25,7 @@ describe('JiraIssuesSidebar', () => { ...@@ -25,10 +25,7 @@ describe('JiraIssuesSidebar', () => {
}; };
afterEach(() => { afterEach(() => {
if (wrapper) { wrapper.destroy();
wrapper.destroy();
wrapper = null;
}
}); });
const findLabelsSelect = () => wrapper.findComponent(LabelsSelect); const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
......
...@@ -41,3 +41,5 @@ export const mockJiraIssueComment = { ...@@ -41,3 +41,5 @@ export const mockJiraIssueComment = {
}, },
id: 10000, id: 10000,
}; };
export const mockJiraIssueStatuses = [{ title: 'In Progress' }, { title: 'Done' }];
...@@ -22,7 +22,6 @@ describe('ManageFeature component', () => { ...@@ -22,7 +22,6 @@ describe('ManageFeature component', () => {
provide: { provide: {
glFeatures: { glFeatures: {
secDependencyScanningUiEnable: true, secDependencyScanningUiEnable: true,
secSecretDetectionUiEnable: true,
}, },
}, },
...options, ...options,
...@@ -80,7 +79,6 @@ describe('ManageFeature component', () => { ...@@ -80,7 +79,6 @@ describe('ManageFeature component', () => {
it.each` it.each`
type | featureFlag type | featureFlag
${REPORT_TYPE_DEPENDENCY_SCANNING} | ${'secDependencyScanningUiEnable'} ${REPORT_TYPE_DEPENDENCY_SCANNING} | ${'secDependencyScanningUiEnable'}
${REPORT_TYPE_SECRET_DETECTION} | ${'secSecretDetectionUiEnable'}
`('renders generic component for $type if $featureFlag is disabled', ({ type, featureFlag }) => { `('renders generic component for $type if $featureFlag is disabled', ({ type, featureFlag }) => {
const [feature] = generateFeatures(1, { type }); const [feature] = generateFeatures(1, { type });
createComponent({ createComponent({
......
...@@ -167,8 +167,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do ...@@ -167,8 +167,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
it 'detects security jobs even when the job has more than one report' do it 'detects security jobs even when the job has more than one report' do
config = { artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } } config = { artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } }
complicated_metadata = double(:complicated_metadata, config_options: config) complicated_job = build_stubbed(:ci_build, options: config)
complicated_job = double(:complicated_job, metadata: complicated_metadata)
allow_next_instance_of(::Security::SecurityJobsFinder) do |finder| allow_next_instance_of(::Security::SecurityJobsFinder) do |finder|
allow(finder).to receive(:execute).and_return([complicated_job]) allow(finder).to receive(:execute).and_return([complicated_job])
......
...@@ -47,7 +47,7 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -47,7 +47,7 @@ RSpec.describe Ci::CreatePipelineService do
end end
it 'persists cross_dependencies' do it 'persists cross_dependencies' do
deps = build_job.options['cross_dependencies'] deps = build_job.options[:cross_dependencies]
result = [ result = [
{ job: "job-1", ref: "ref-1", project: "project-1", artifacts: true }, { job: "job-1", ref: "ref-1", project: "project-1", artifacts: true },
{ job: "job-2", ref: "ref-2", project: "project-2", artifacts: false }, { job: "job-2", ref: "ref-2", project: "project-2", artifacts: false },
...@@ -142,7 +142,7 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -142,7 +142,7 @@ RSpec.describe Ci::CreatePipelineService do
end end
it 'persists cross_dependencies' do it 'persists cross_dependencies' do
deps = test_job.options['cross_dependencies'] deps = test_job.options[:cross_dependencies]
result = { result = {
job: 'dependency', job: 'dependency',
ref: 'master', ref: 'master',
......
...@@ -96,7 +96,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -96,7 +96,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
expect(bridge).to be_a Ci::Bridge expect(bridge).to be_a Ci::Bridge
expect(bridge.stage).to eq 'deploy' expect(bridge.stage).to eq 'deploy'
expect(pipeline.statuses).to match_array [test, bridge] expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq('trigger' => { 'project' => 'my/project' }) expect(bridge.options).to eq(trigger: { project: 'my/project' })
expect(bridge.yaml_variables) expect(bridge.yaml_variables)
.to include(key: 'CROSS', value: 'downstream', public: true) .to include(key: 'CROSS', value: 'downstream', public: true)
end end
......
...@@ -83,15 +83,15 @@ RSpec.describe Ci::RunDastScanService do ...@@ -83,15 +83,15 @@ RSpec.describe Ci::RunDastScanService do
it 'creates a build with appropriate options' do it 'creates a build with appropriate options' do
build = pipeline.builds.first build = pipeline.builds.first
expected_options = { expected_options = {
'image' => { image: {
'name' => '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION' name: '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION'
}, },
'script' => [ script: [
'/analyze' '/analyze'
], ],
'artifacts' => { artifacts: {
'reports' => { reports: {
'dast' => ['gl-dast-report.json'] dast: ['gl-dast-report.json']
} }
} }
} }
...@@ -103,61 +103,61 @@ RSpec.describe Ci::RunDastScanService do ...@@ -103,61 +103,61 @@ RSpec.describe Ci::RunDastScanService do
expected_variables = [ expected_variables = [
{ {
'key' => 'DAST_AUTH_URL', key: 'DAST_AUTH_URL',
'value' => dast_site_profile.auth_url, value: dast_site_profile.auth_url,
'public' => true public: true
}, { }, {
'key' => 'DAST_DEBUG', key: 'DAST_DEBUG',
'value' => String(dast_scanner_profile.show_debug_messages?), value: String(dast_scanner_profile.show_debug_messages?),
'public' => true public: true
}, { }, {
'key' => 'DAST_EXCLUDE_URLS', key: 'DAST_EXCLUDE_URLS',
'value' => dast_site_profile.excluded_urls.join(','), value: dast_site_profile.excluded_urls.join(','),
'public' => true public: true
}, { }, {
'key' => 'DAST_FULL_SCAN_ENABLED', key: 'DAST_FULL_SCAN_ENABLED',
'value' => String(dast_scanner_profile.full_scan_enabled?), value: String(dast_scanner_profile.full_scan_enabled?),
'public' => true public: true
}, { }, {
'key' => 'DAST_PASSWORD_FIELD', key: 'DAST_PASSWORD_FIELD',
'value' => dast_site_profile.auth_password_field, value: dast_site_profile.auth_password_field,
'public' => true public: true
}, { }, {
'key' => 'DAST_SPIDER_MINS', key: 'DAST_SPIDER_MINS',
'value' => String(dast_scanner_profile.spider_timeout), value: String(dast_scanner_profile.spider_timeout),
'public' => true public: true
}, { }, {
'key' => 'DAST_TARGET_AVAILABILITY_TIMEOUT', key: 'DAST_TARGET_AVAILABILITY_TIMEOUT',
'value' => String(dast_scanner_profile.target_timeout), value: String(dast_scanner_profile.target_timeout),
'public' => true public: true
}, { }, {
'key' => 'DAST_USERNAME', key: 'DAST_USERNAME',
'value' => dast_site_profile.auth_username, value: dast_site_profile.auth_username,
'public' => true public: true
}, { }, {
'key' => 'DAST_USERNAME_FIELD', key: 'DAST_USERNAME_FIELD',
'value' => dast_site_profile.auth_username_field, value: dast_site_profile.auth_username_field,
'public' => true public: true
}, { }, {
'key' => 'DAST_USE_AJAX_SPIDER', key: 'DAST_USE_AJAX_SPIDER',
'value' => String(dast_scanner_profile.use_ajax_spider?), value: String(dast_scanner_profile.use_ajax_spider?),
'public' => true public: true
}, { }, {
'key' => 'DAST_VERSION', key: 'DAST_VERSION',
'value' => '1', value: '1',
'public' => true public: true
}, { }, {
'key' => 'DAST_WEBSITE', key: 'DAST_WEBSITE',
'value' => dast_site_profile.dast_site.url, value: dast_site_profile.dast_site.url,
'public' => true public: true
}, { }, {
'key' => 'GIT_STRATEGY', key: 'GIT_STRATEGY',
'value' => 'none', value: 'none',
'public' => true public: true
}, { }, {
'key' => 'SECURE_ANALYZERS_PREFIX', key: 'SECURE_ANALYZERS_PREFIX',
'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', value: 'registry.gitlab.com/gitlab-org/security-products/analyzers',
'public' => true public: true
} }
] ]
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Background migration for deleting orphaned deployments.
class DeleteOrphanedDeployments
include Database::MigrationHelpers
def perform(start_id, end_id)
orphaned_deployments
.where(id: start_id..end_id)
.delete_all
mark_job_as_succeeded(start_id, end_id)
end
def orphaned_deployments
define_batchable_model('deployments')
.where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
private
def mark_job_as_succeeded(*arguments)
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
self.class.name.demodulize,
arguments
)
end
end
end
end
...@@ -189,7 +189,7 @@ module Gitlab ...@@ -189,7 +189,7 @@ module Gitlab
raise ArchiveError, 'Job is not finished yet' unless job.complete? raise ArchiveError, 'Job is not finished yet' unless job.complete?
if trace_artifact if trace_artifact
unsafe_trace_cleanup! if Feature.enabled?(:erase_traces_from_already_archived_jobs_when_archiving_again, job.project, default_enabled: :yaml) unsafe_trace_cleanup!
raise AlreadyArchivedError, 'Could not archive again' raise AlreadyArchivedError, 'Could not archive again'
end end
......
...@@ -27,21 +27,18 @@ module Gitlab ...@@ -27,21 +27,18 @@ module Gitlab
# connect with Spamcheck # connect with Spamcheck
@endpoint_url = @endpoint_url.gsub(%r(^grpc:\/\/), '') @endpoint_url = @endpoint_url.gsub(%r(^grpc:\/\/), '')
creds = @creds =
if Rails.env.development? || Rails.env.test? if Rails.env.development? || Rails.env.test?
:this_channel_is_insecure :this_channel_is_insecure
else else
GRPC::Core::ChannelCredentials.new GRPC::Core::ChannelCredentials.new
end end
@stub = ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, creds,
timeout: DEFAULT_TIMEOUT_SECS)
end end
def issue_spam?(spam_issue:, user:, context: {}) def issue_spam?(spam_issue:, user:, context: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context) issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
response = @stub.check_for_spam_issue(issue, response = grpc_client.check_for_spam_issue(issue,
metadata: { 'authorization' => metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key }) Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict) verdict = convert_verdict_to_gitlab_constant(response.verdict)
...@@ -100,6 +97,16 @@ module Gitlab ...@@ -100,6 +97,16 @@ module Gitlab
Google::Protobuf::Timestamp.new(seconds: ar_timestamp.to_time.to_i, Google::Protobuf::Timestamp.new(seconds: ar_timestamp.to_time.to_i,
nanos: ar_timestamp.to_time.nsec) nanos: ar_timestamp.to_time.nsec)
end end
def grpc_client
@grpc_client ||= ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, @creds,
interceptors: interceptors,
timeout: DEFAULT_TIMEOUT_SECS)
end
def interceptors
[Labkit::Correlation::GRPC::ClientInterceptor.instance]
end
end end
end end
end end
...@@ -169,6 +169,16 @@ module Gitlab ...@@ -169,6 +169,16 @@ module Gitlab
end end
end end
def deep_symbolized_access(data)
if data.is_a?(Array)
data.map(&method(:deep_symbolized_access))
elsif data.is_a?(Hash)
data.deep_symbolize_keys
else
data
end
end
def string_to_ip_object(str) def string_to_ip_object(str)
return unless str return unless str
......
# frozen_string_literal: true
module Serializers
# Make the resulting hash have deep symbolized keys
class SymbolizedJson
class << self
def dump(obj)
obj
end
def load(data)
return if data.nil?
Gitlab::Utils.deep_symbolized_access(data)
end
end
end
end
...@@ -18406,9 +18406,15 @@ msgstr "" ...@@ -18406,9 +18406,15 @@ msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled." msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "" msgstr ""
msgid "JiraService|Failed to load Jira issue statuses. View the issue in Jira, or reload the page."
msgstr ""
msgid "JiraService|Failed to load Jira issue. View the issue in Jira, or reload the page." msgid "JiraService|Failed to load Jira issue. View the issue in Jira, or reload the page."
msgstr "" msgstr ""
msgid "JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page."
msgstr ""
msgid "JiraService|Fetch issue types for this Jira project" msgid "JiraService|Fetch issue types for this Jira project"
msgstr "" msgstr ""
...@@ -18457,6 +18463,9 @@ msgstr "" ...@@ -18457,6 +18463,9 @@ msgstr ""
msgid "JiraService|Move to Done" msgid "JiraService|Move to Done"
msgstr "" msgstr ""
msgid "JiraService|No available statuses"
msgstr ""
msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}." msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}."
msgstr "" msgstr ""
......
...@@ -60,6 +60,7 @@ module DeprecationToolkitEnv ...@@ -60,6 +60,7 @@ module DeprecationToolkitEnv
activerecord-6.0.3.7/lib/active_record/relation.rb activerecord-6.0.3.7/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb
gitlab-labkit-0.18.0/lib/labkit/correlation/grpc/client_interceptor.rb
] ]
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migration, schema: 20210617161348 do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:environment) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
let(:background_migration_jobs) { table(:background_migration_jobs) }
before do
create_deployment!(environment.id, project.id)
create_deployment!(non_existing_record_id, project.id)
end
it 'deletes only orphaned deployments' do
expect(valid_deployments.pluck(:id)).not_to be_empty
expect(orphaned_deployments.pluck(:id)).not_to be_empty
subject.perform(table(:deployments).minimum(:id), table(:deployments).maximum(:id))
expect(valid_deployments.pluck(:id)).not_to be_empty
expect(orphaned_deployments.pluck(:id)).to be_empty
end
it 'marks jobs as done' do
first_job = background_migration_jobs.create!(
class_name: 'DeleteOrphanedDeployments',
arguments: [table(:deployments).minimum(:id), table(:deployments).minimum(:id)]
)
second_job = background_migration_jobs.create!(
class_name: 'DeleteOrphanedDeployments',
arguments: [table(:deployments).maximum(:id), table(:deployments).maximum(:id)]
)
subject.perform(table(:deployments).minimum(:id), table(:deployments).minimum(:id))
expect(first_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:succeeded])
expect(second_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:pending])
end
private
def valid_deployments
table(:deployments).where('EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
def orphaned_deployments
table(:deployments).where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
def create_deployment!(environment_id, project_id)
table(:deployments).create!(
environment_id: environment_id,
project_id: project_id,
ref: 'master',
tag: false,
sha: 'x',
status: 1,
iid: table(:deployments).count + 1)
end
end
...@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Spamcheck::Client do ...@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
let(:endpoint) { 'grpc://grpc.test.url' } let(:endpoint) { 'grpc://grpc.test.url' }
let_it_be(:user) { create(:user, organization: 'GitLab') } let_it_be(:user) { create(:user, organization: 'GitLab') }
let(:verdict_value) { nil } let(:verdict_value) { ::Spamcheck::SpamVerdict::Verdict::ALLOW }
let(:error_value) { "" } let(:error_value) { "" }
let(:attribs_value) do let(:attribs_value) do
...@@ -56,6 +56,13 @@ RSpec.describe Gitlab::Spamcheck::Client do ...@@ -56,6 +56,13 @@ RSpec.describe Gitlab::Spamcheck::Client do
expect(subject).to eq([expected, { "monitorMode" => "false" }, ""]) expect(subject).to eq([expected, { "monitorMode" => "false" }, ""])
end end
end end
it 'includes interceptors' do
expect_next_instance_of(::Gitlab::Spamcheck::Client) do |client|
expect(client).to receive(:interceptors).and_call_original
end
subject
end
end end
describe "#build_issue_protobuf", :aggregate_failures do describe "#build_issue_protobuf", :aggregate_failures do
......
...@@ -351,6 +351,22 @@ RSpec.describe Gitlab::Utils do ...@@ -351,6 +351,22 @@ RSpec.describe Gitlab::Utils do
end end
end end
describe '.deep_symbolized_access' do
let(:hash) do
{ "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
end
subject { described_class.deep_symbolized_access(hash) }
it 'allows to access hash keys with symbols' do
expect(subject[:variables]).to be_a(Array)
end
it 'allows to access array keys with symbols' do
expect(subject[:variables].first[:key]).to eq('VAR1')
end
end
describe '.try_megabytes_to_bytes' do describe '.try_megabytes_to_bytes' do
context 'when the size can be converted to megabytes' do context 'when the size can be converted to megabytes' do
it 'returns the size in megabytes' do it 'returns the size in megabytes' do
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Serializers::SymbolizedJson do
describe '.dump' do
let(:obj) { { key: "value" } }
subject { described_class.dump(obj) }
it 'returns a hash' do
is_expected.to eq(obj)
end
end
describe '.load' do
let(:data_string) { '{"key":"value","variables":[{"key":"VAR1","value":"VALUE1"}]}' }
let(:data_hash) { Gitlab::Json.parse(data_string) }
context 'when loading a hash' do
subject { described_class.load(data_hash) }
it 'decodes a string' do
is_expected.to be_a(Hash)
end
it 'allows to access with symbols' do
expect(subject[:key]).to eq('value')
expect(subject[:variables].first[:key]).to eq('VAR1')
end
end
context 'when loading a nil' do
subject { described_class.load(nil) }
it 'returns nil' do
is_expected.to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleDeleteOrphanedDeployments, :sidekiq, schema: 20210617161348 do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:environment) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
let(:background_migration_jobs) { table(:background_migration_jobs) }
before do
create_deployment!(environment.id, project.id)
create_deployment!(environment.id, project.id)
create_deployment!(environment.id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'schedules DeleteOrphanedDeployments background jobs' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(7)
table(:deployments).find_each do |deployment|
expect(described_class::MIGRATION).to be_scheduled_migration(deployment.id, deployment.id)
end
end
end
end
def create_deployment!(environment_id, project_id)
table(:deployments).create!(
environment_id: environment_id,
project_id: project_id,
ref: 'master',
tag: false,
sha: 'x',
status: 1,
iid: table(:deployments).count + 1)
end
end
...@@ -2172,15 +2172,15 @@ RSpec.describe Ci::Build do ...@@ -2172,15 +2172,15 @@ RSpec.describe Ci::Build do
end end
it 'contains options' do it 'contains options' do
expect(build.options).to eq(options.stringify_keys) expect(build.options).to eq(options.symbolize_keys)
end end
it 'allows to access with keys' do it 'allows to access with symbolized keys' do
expect(build.options[:image]).to eq('ruby:2.7') expect(build.options[:image]).to eq('ruby:2.7')
end end
it 'allows to access with strings' do it 'rejects access with string keys' do
expect(build.options['image']).to eq('ruby:2.7') expect(build.options['image']).to be_nil
end end
context 'when ci_build_metadata_config is set' do context 'when ci_build_metadata_config is set' do
...@@ -2189,7 +2189,7 @@ RSpec.describe Ci::Build do ...@@ -2189,7 +2189,7 @@ RSpec.describe Ci::Build do
end end
it 'persist data in build metadata' do it 'persist data in build metadata' do
expect(build.metadata.read_attribute(:config_options)).to eq(options.stringify_keys) expect(build.metadata.read_attribute(:config_options)).to eq(options.symbolize_keys)
end end
it 'does not persist data in build' do it 'does not persist data in build' do
...@@ -4715,9 +4715,9 @@ RSpec.describe Ci::Build do ...@@ -4715,9 +4715,9 @@ RSpec.describe Ci::Build do
describe '#read_metadata_attribute' do describe '#read_metadata_attribute' do
let(:build) { create(:ci_build, :degenerated) } let(:build) { create(:ci_build, :degenerated) }
let(:build_options) { { "key" => "build" } } let(:build_options) { { key: "build" } }
let(:metadata_options) { { "key" => "metadata" } } let(:metadata_options) { { key: "metadata" } }
let(:default_options) { { "key" => "default" } } let(:default_options) { { key: "default" } }
subject { build.send(:read_metadata_attribute, :options, :config_options, default_options) } subject { build.send(:read_metadata_attribute, :options, :config_options, default_options) }
...@@ -4752,8 +4752,8 @@ RSpec.describe Ci::Build do ...@@ -4752,8 +4752,8 @@ RSpec.describe Ci::Build do
describe '#write_metadata_attribute' do describe '#write_metadata_attribute' do
let(:build) { create(:ci_build, :degenerated) } let(:build) { create(:ci_build, :degenerated) }
let(:options) { { "key" => "new options" } } let(:options) { { key: "new options" } }
let(:existing_options) { { "key" => "existing options" } } let(:existing_options) { { key: "existing options" } }
subject { build.send(:write_metadata_attribute, :options, :config_options, options) } subject { build.send(:write_metadata_attribute, :options, :config_options, options) }
......
...@@ -30,43 +30,17 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do ...@@ -30,43 +30,17 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
create(:ci_build_trace_chunk, build: job) create(:ci_build_trace_chunk, build: job)
end end
context 'when the feature flag `erase_traces_from_already_archived_jobs_when_archiving_again` is enabled' do it 'removes the trace chunks' do
before do expect { subject }.to change { job.trace_chunks.count }.to(0)
stub_feature_flags(erase_traces_from_already_archived_jobs_when_archiving_again: true)
end
it 'removes the trace chunks' do
expect { subject }.to change { job.trace_chunks.count }.to(0)
end
context 'when associated data does not exist' do
before do
job.job_artifacts_trace.file.remove!
end
it 'removes the trace artifact' do
expect { subject }.to change { job.reload.job_artifacts_trace }.to(nil)
end
end
end end
context 'when the feature flag `erase_traces_from_already_archived_jobs_when_archiving_again` is disabled' do context 'when associated data does not exist' do
before do before do
stub_feature_flags(erase_traces_from_already_archived_jobs_when_archiving_again: false) job.job_artifacts_trace.file.remove!
end end
it 'does not remove the trace chunks' do it 'removes the trace artifact' do
expect { subject }.not_to change { job.trace_chunks.count } expect { subject }.to change { job.reload.job_artifacts_trace }.to(nil)
end
context 'when associated data does not exist' do
before do
job.job_artifacts_trace.file.remove!
end
it 'does not remove the trace artifact' do
expect { subject }.not_to change { job.reload.job_artifacts_trace }
end
end end
end end
end end
......
...@@ -33,11 +33,11 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -33,11 +33,11 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses the provided key' do it 'uses the provided key' do
expected = { expected = {
'key' => 'a-key', key: 'a-key',
'paths' => ['logs/', 'binaries/'], paths: ['logs/', 'binaries/'],
'policy' => 'pull-push', policy: 'pull-push',
'untracked' => true, untracked: true,
'when' => 'on_success' when: 'on_success'
} }
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
...@@ -66,10 +66,10 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -66,10 +66,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'builds a cache key' do it 'builds a cache key' do
expected = { expected = {
'key' => /[a-f0-9]{40}/, key: /[a-f0-9]{40}/,
'paths' => ['logs/'], paths: ['logs/'],
'policy' => 'pull-push', policy: 'pull-push',
'when' => 'on_success' when: 'on_success'
} }
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
...@@ -82,10 +82,10 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -82,10 +82,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses default cache key' do it 'uses default cache key' do
expected = { expected = {
'key' => /default/, key: /default/,
'paths' => ['logs/'], paths: ['logs/'],
'policy' => 'pull-push', policy: 'pull-push',
'when' => 'on_success' when: 'on_success'
} }
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
...@@ -115,10 +115,10 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -115,10 +115,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'builds a cache key' do it 'builds a cache key' do
expected = { expected = {
'key' => /\$ENV_VAR-[a-f0-9]{40}/, key: /\$ENV_VAR-[a-f0-9]{40}/,
'paths' => ['logs/'], paths: ['logs/'],
'policy' => 'pull-push', policy: 'pull-push',
'when' => 'on_success' when: 'on_success'
} }
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
...@@ -131,10 +131,10 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -131,10 +131,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses default cache key' do it 'uses default cache key' do
expected = { expected = {
'key' => /\$ENV_VAR-default/, key: /\$ENV_VAR-default/,
'paths' => ['logs/'], paths: ['logs/'],
'policy' => 'pull-push', policy: 'pull-push',
'when' => 'on_success' when: 'on_success'
} }
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
......
...@@ -39,8 +39,8 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -39,8 +39,8 @@ RSpec.describe Ci::CreatePipelineService do
it 'creates a pipeline' do it 'creates a pipeline' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(pipeline.builds.first.options).to match(a_hash_including({ expect(pipeline.builds.first.options).to match(a_hash_including({
'before_script' => ['ls'], before_script: ['ls'],
'script' => [ script: [
'echo doing my first step', 'echo doing my first step',
'echo doing step 1 of job 1', 'echo doing step 1 of job 1',
'echo doing my last step' 'echo doing my last step'
......
...@@ -104,7 +104,7 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -104,7 +104,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'saves dependencies' do it 'saves dependencies' do
expect(test_a_build.options) expect(test_a_build.options)
.to match(a_hash_including('dependencies' => ['build_a'])) .to match(a_hash_including(dependencies: ['build_a']))
end end
it 'artifacts default to true' do it 'artifacts default to true' do
......
...@@ -69,9 +69,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -69,9 +69,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => [ include: [
{ 'local' => 'path/to/child.yml' } { local: 'path/to/child.yml' }
] ]
} }
} }
...@@ -149,9 +149,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -149,9 +149,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => [ include: [
{ 'local' => 'path/to/child.yml' } { local: 'path/to/child.yml' }
] ]
} }
} }
...@@ -175,8 +175,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -175,8 +175,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => 'path/to/child.yml' include: 'path/to/child.yml'
} }
} }
end end
...@@ -202,8 +202,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -202,8 +202,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => ['path/to/child.yml', 'path/to/child2.yml'] include: ['path/to/child.yml', 'path/to/child2.yml']
} }
} }
end end
...@@ -295,12 +295,12 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -295,12 +295,12 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => [ include: [
{ {
'file' => 'path/to/child.yml', file: 'path/to/child.yml',
'project' => 'my-namespace/my-project', project: 'my-namespace/my-project',
'ref' => 'master' ref: 'master'
} }
] ]
} }
...@@ -353,11 +353,11 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -353,11 +353,11 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do it_behaves_like 'successful creation' do
let(:expected_bridge_options) do let(:expected_bridge_options) do
{ {
'trigger' => { trigger: {
'include' => [ include: [
{ {
'file' => ["path/to/child1.yml", "path/to/child2.yml"], file: ["path/to/child1.yml", "path/to/child2.yml"],
'project' => 'my-namespace/my-project' project: 'my-namespace/my-project'
} }
] ]
} }
......
...@@ -1001,7 +1001,7 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -1001,7 +1001,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(pipeline.yaml_errors).not_to be_present expect(pipeline.yaml_errors).not_to be_present
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(build).to be_kind_of(Ci::Build) expect(build).to be_kind_of(Ci::Build)
expect(build.options).to eq(config[:release].except(:stage, :only).with_indifferent_access) expect(build.options).to eq(config[:release].except(:stage, :only))
expect(build).to be_persisted expect(build).to be_persisted
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment