summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue18
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue10
-rw-r--r--app/assets/javascripts/alert_management/constants.js9
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue36
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js15
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss (renamed from app/assets/stylesheets/page_bundles/themes/_dark.scss)205
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_dark.scss45
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb24
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/graphql/mutations/alert_management/update_alert_status.rb5
-rw-r--r--app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb1
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb3
-rw-r--r--app/models/group.rb5
-rw-r--r--app/models/member.rb1
-rw-r--r--app/serializers/diff_file_base_entity.rb22
-rw-r--r--app/serializers/diffs_entity.rb4
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb2
-rw-r--r--app/serializers/issuable_sidebar_extras_entity.rb2
-rw-r--r--app/serializers/merge_request_assignee_entity.rb2
-rw-r--r--app/services/alert_management/update_alert_status_service.rb23
-rw-r--r--app/services/authorized_project_update/project_create_service.rb34
-rw-r--r--app/workers/all_queues.yml7
-rw-r--r--app/workers/authorized_project_update/project_create_worker.rb19
-rw-r--r--changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml5
-rw-r--r--changelogs/unreleased/move-build-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move-deploy-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml5
-rw-r--r--changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml6
-rw-r--r--changelogs/unreleased/pages-1-18.yml5
-rw-r--r--changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml5
-rw-r--r--changelogs/unreleased/revert-0ddee28a.yml5
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json4
-rw-r--r--doc/ci/examples/test-scala-application.md2
-rw-r--r--doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.pngbin0 -> 91436 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png (renamed from doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png)bin9766 -> 9766 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md18
-rw-r--r--lib/api/entities/user_basic.rb2
-rw-r--r--lib/api/entities/user_path.rb2
-rw-r--r--lib/api/metrics/dashboard/annotations.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml149
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Scala.gitlab-ci.yml8
-rw-r--r--locale/gitlab.pot24
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb29
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb3
-rw-r--r--spec/factories/alert_management/alerts.rb6
-rw-r--r--spec/fixtures/api/schemas/entities/discussion.json11
-rw-r--r--spec/fixtures/api/schemas/entities/note_user_entity.json3
-rw-r--r--spec/fixtures/api/schemas/entities/user.json3
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issue.json9
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/members.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/snippets.json4
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js42
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js15
-rw-r--r--spec/frontend/alert_management/mocks/alerts.json6
-rw-r--r--spec/frontend/diffs/components/edit_button_spec.js19
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js6
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb222
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb6
-rw-r--r--spec/models/group_spec.rb36
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb72
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb90
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/serializers/diff_file_base_entity_spec.rb58
-rw-r--r--spec/serializers/diffs_metadata_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_sidebar_basic_entity_spec.rb2
-rw-r--r--spec/services/alert_management/update_alert_status_service_spec.rb59
-rw-r--r--spec/services/authorized_project_update/project_create_service_spec.rb142
-rw-r--r--spec/workers/authorized_project_update/project_create_worker_spec.rb50
84 files changed, 1512 insertions, 444 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 092afa15df4..84cc529467b 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.17.0
+1.18.0
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index ef060713d3d..cee1988e3ec 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -1,5 +1,12 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem, GlTabs, GlTab, GlButton } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlTabs,
+ GlTab,
+ GlButton,
+} from '@gitlab/ui';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
@@ -16,6 +23,7 @@ export default {
overviewTitle: s__('AlertManagement|Overview'),
},
components: {
+ GlLoadingIcon,
GlNewDropdown,
GlNewDropdownItem,
GlTab,
@@ -55,10 +63,16 @@ export default {
data() {
return { alert: null };
},
+ computed: {
+ loading() {
+ return this.$apollo.queries.alert.loading;
+ },
+ },
};
</script>
<template>
<div>
+ <div v-if="loading"><gl-loading-icon size="lg" class="mt-3" /></div>
<div
v-if="alert"
class="gl-display-flex justify-content-end gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid gl-p-4"
@@ -73,7 +87,7 @@ export default {
{{ s__('AlertManagement|Create issue') }}
</gl-button>
</div>
- <div class="gl-display-flex justify-content-end">
+ <div v-if="alert" class="gl-display-flex justify-content-end">
<gl-new-dropdown right>
<gl-new-dropdown-item
v-for="(label, field) in $options.statuses"
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index 7fe74eb1da8..a2efa6f0e0c 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -15,7 +15,7 @@ import {
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import getAlerts from '../graphql/queries/getAlerts.query.graphql';
-import { ALERTS_STATUS, ALERTS_STATUS_TABS } from '../constants';
+import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
@@ -68,6 +68,7 @@ export default {
[ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'),
[ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'),
},
+ severityLabels: ALERTS_SEVERITY_LABELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
GlEmptyState,
@@ -185,14 +186,17 @@ export default {
stacked="md"
>
<template #cell(severity)="{ item }">
- <div class="d-inline-flex align-items-center justify-content-between">
+ <div
+ class="d-inline-flex align-items-center justify-content-between"
+ data-testid="severityField"
+ >
<gl-icon
class="mr-2"
:size="12"
:name="`severity-${item.severity.toLowerCase()}`"
:class="`icon-${item.severity.toLowerCase()}`"
/>
- {{ item.severity }}
+ {{ $options.severityLabels[item.severity] }}
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index c95a3c29f04..ddaf8242b68 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -1,5 +1,14 @@
import { s__ } from '~/locale';
+export const ALERTS_SEVERITY_LABELS = {
+ CRITICAL: s__('AlertManagement|Critical'),
+ HIGH: s__('AlertManagement|High'),
+ MEDIUM: s__('AlertManagement|Medium'),
+ LOW: s__('AlertManagement|Low'),
+ INFO: s__('AlertManagement|Info'),
+ UNKNOWN: s__('AlertManagement|Unknown'),
+};
+
export const ALERTS_STATUS = {
OPEN: 'open',
TRIGGERED: 'triggered',
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index 91e296f8572..21fdb19287d 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -13,7 +14,8 @@ export default {
props: {
editPath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
canCurrentUserFork: {
type: Boolean,
@@ -25,6 +27,18 @@ export default {
default: false,
},
},
+ computed: {
+ tooltipTitle() {
+ if (this.isDisabled) {
+ return __("Can't edit as source branch was deleted");
+ }
+
+ return __('Edit file');
+ },
+ isDisabled() {
+ return !this.editPath;
+ },
+ },
methods: {
handleEditClick(evt) {
if (this.canCurrentUserFork && !this.canModifyBlob) {
@@ -37,13 +51,15 @@ export default {
</script>
<template>
- <gl-deprecated-button
- v-gl-tooltip.top
- :href="editPath"
- :title="__('Edit file')"
- class="js-edit-blob"
- @click.native="handleEditClick"
- >
- <icon name="pencil" />
- </gl-deprecated-button>
+ <span v-gl-tooltip.top :title="tooltipTitle">
+ <gl-deprecated-button
+ :href="editPath"
+ :disabled="isDisabled"
+ :class="{ 'cursor-not-allowed': isDisabled }"
+ class="rounded-0 js-edit-blob"
+ @click.native="handleEditClick"
+ >
+ <icon name="pencil" />
+ </gl-deprecated-button>
+ </span>
</template>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 0134378868b..903105babeb 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -13,11 +13,7 @@ import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import statusCodes from '../../lib/utils/http_status';
-import {
- backOff,
- convertObjectPropsToCamelCase,
- isFeatureFlagEnabled,
-} from '../../lib/utils/common_utils';
+import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import {
@@ -116,14 +112,7 @@ export const clearExpandedPanel = ({ commit }) => {
export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard');
- /**
- * Annotations data is not yet fetched. This will be
- * ready after the BE piece is implemented.
- * https://gitlab.com/gitlab-org/gitlab/-/issues/211330
- */
- if (isFeatureFlagEnabled('metricsDashboardAnnotations')) {
- dispatch('fetchAnnotations');
- }
+ dispatch('fetchAnnotations');
};
// Metrics dashboard
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index e4466b44358..c9a8d5e5975 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -85,6 +85,10 @@ $item-weight-max-width: 48px;
white-space: nowrap;
}
+ .health-label-short {
+ display: none;
+ }
+
@include media-breakpoint-down(lg) {
.issue-count-badge {
padding: 0;
@@ -96,7 +100,6 @@ $item-weight-max-width: 48px;
.item-body,
.card-header {
.health-label-short {
- display: initial;
max-width: 0;
}
@@ -131,6 +134,12 @@ $item-weight-max-width: 48px;
}
}
+.card-header {
+ .health-label-short {
+ display: initial;
+ }
+}
+
.item-meta {
flex-basis: 100%;
font-size: $gl-font-size;
@@ -265,7 +274,6 @@ $item-weight-max-width: 48px;
max-width: 90%;
}
- .item-body,
.card-header {
.health-label-short {
max-width: 30px;
@@ -306,7 +314,6 @@ $item-weight-max-width: 48px;
}
}
- .item-body,
.card-header {
.health-label-short {
max-width: 60px;
@@ -326,7 +333,6 @@ $item-weight-max-width: 48px;
}
}
- .item-body,
.card-header {
.health-label-short {
max-width: 100px;
@@ -364,10 +370,6 @@ $item-weight-max-width: 48px;
}
}
- .health-label-short {
- display: initial;
- }
-
.health-label-long {
display: none;
}
@@ -411,8 +413,7 @@ $item-weight-max-width: 48px;
}
@media only screen and (min-width: 1500px) {
- .card-header,
- .item-body {
+ .card-header {
.health-label-short {
display: none;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ecf2097dc87..f47d0cab31f 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -507,6 +507,10 @@
opacity: 1 !important;
cursor: default !important;
+ &.cursor-not-allowed {
+ cursor: not-allowed !important;
+ }
+
i {
color: $gl-text-color-disabled !important;
}
diff --git a/app/assets/stylesheets/page_bundles/themes/_dark.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 1d316ca2e3f..86dffb4d7df 100644
--- a/app/assets/stylesheets/page_bundles/themes/_dark.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -1,40 +1,6 @@
.ide.theme-dark {
- $border-color: #1d1f21;
- $highlight-accent: #fff;
- $text-color: #ccc;
- $background: #333;
- $background-hover: #2d2d2d;
- $highlight-background: #252526;
- $link-color: #428fdc;
- $footer-background: #060606;
-
- $input-border: #868686;
- $input-background: transparent;
- $input-color: $highlight-accent;
-
- $btn-default-background: transparent;
- $btn-default-border: #bfbfbf;
- $btn-default-hover-border: #d8d8d8;
-
- $btn-primary-background: #1068bf;
- $btn-primary-border: #428fdc;
- $btn-primary-hover-border: #63a6e9;
-
- $btn-success-background: #217645;
- $btn-success-border: #108548;
- $btn-success-hover-border: #2da160;
-
- $btn-disabled-border: rgba(223, 223, 223, 0.24);
- $btn-disabled-color: rgba(145, 145, 145, 0.48);
-
- $dropdown-background: #404040;
- $dropdown-hover-background: #525252;
-
- $diff-insert: rgba(155, 185, 85, 0.2);
- $diff-remove: rgba(255, 0, 0, 0.2);
-
a:not(.btn) {
- color: $link-color;
+ color: var(--ide-link-color);
}
h1,
@@ -72,12 +38,12 @@
.ide-navigator-btn,
.ide-pipeline .top-bar,
.ide-pipeline .top-bar .controllers .controllers-buttons {
- color: $text-color;
+ color: var(--ide-text-color);
}
.drag-handle:hover,
.card-header .badge.badge-pill {
- background-color: $dropdown-hover-background;
+ background-color: var(--ide-dropdown-hover-background);
}
.dropdown-menu-toggle svg,
@@ -86,38 +52,34 @@
.file-row .file-row-icon svg,
.file-row:hover .file-row-icon svg,
.controllers-buttons svg {
- fill: $text-color;
+ fill: var(--ide-text-color);
}
.ide-pipeline svg {
- --svg-status-bg: $background;
+ --svg-status-bg: var(--ide-background);
}
.multi-file-tab-close:hover {
- background-color: $input-border;
+ background-color: var(--ide-input-border);
}
.ide-review-sub-header:hover {
- color: $input-border;
+ color: var(--ide-input-border);
}
.text-secondary {
- color: $text-color !important;
+ color: var(--ide-text-color) !important;
}
input[type='search']::placeholder,
input[type='text']::placeholder,
textarea::placeholder,
.dropdown-input .fa {
- color: $input-border;
+ color: var(--ide-input-border);
}
.ide-nav-form .input-icon {
- fill: $input-border;
- }
-
- .ide-staged-action-btn {
- background-color: transparent;
+ fill: var(--ide-input-border);
}
code,
@@ -139,32 +101,28 @@
.bs-callout,
.ide-pipeline .top-bar,
.ide-terminal .top-bar {
- background-color: $background;
- }
-
- pre code {
- background-color: inherit;
+ background-color: var(--ide-background);
}
.bs-callout {
- border-color: $dropdown-background;
+ border-color: var(--ide-dropdown-background);
code {
- background-color: $dropdown-background;
+ background-color: var(--ide-dropdown-background);
}
}
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover {
- border-color: $dropdown-hover-background;
+ border-color: var(--ide-dropdown-hover-background);
}
.ide-sidebar-link:hover,
.multi-file-tabs li {
- background-color: $background-hover;
+ background-color: var(--ide-background-hover);
}
.common-note-form .md-area {
- border-color: $input-border;
+ border-color: var(--ide-input-border);
}
&,
@@ -180,7 +138,7 @@
.card,
.multi-file-commit-panel-success-message,
.ide-preview-header {
- background-color: $highlight-background;
+ background-color: var(--ide-highlight-background);
}
.multi-file-commit-panel {
@@ -204,7 +162,7 @@
.ide-job-item:not(:last-child),
.ide-terminal .top-bar,
.ide-pipeline .top-bar {
- border-color: $border-color;
+ border-color: var(--ide-border-color);
}
.md h1,
@@ -222,58 +180,58 @@
.multi-file-commit-form .nav-links:not(.quick-links),
.ide-pipeline-list .nav-links:not(.quick-links),
.ide-preview-header {
- border-color: $background;
+ border-color: var(--ide-background);
}
.multi-file-tabs li.active {
- border-bottom-color: $highlight-background;
+ border-bottom-color: var(--ide-highlight-background);
}
.multi-file-tabs,
.ide-commit-editor-header {
- box-shadow: inset 0 -1px $border-color;
+ box-shadow: inset 0 -1px var(--ide-border-color);
}
.ide-sidebar-link.active {
- color: $highlight-accent;
- box-shadow: inset 3px 0 $highlight-accent;
+ color: var(--ide-highlight-accent);
+ box-shadow: inset 3px 0 var(--ide-highlight-accent);
&.is-right {
- box-shadow: inset -3px 0 $highlight-accent;
+ box-shadow: inset -3px 0 var(--ide-highlight-accent);
}
}
.nav-links li.active a,
.nav-links li a.active {
- border-color: $highlight-accent;
- color: $text-color;
+ border-color: var(--ide-highlight-accent);
+ color: var(--ide-text-color);
}
.avatar-container {
&,
.avatar {
- color: $text-color;
- background-color: $highlight-background;
- border-color: $highlight-background;
+ color: var(--ide-text-color);
+ background-color: var(--ide-highlight-background);
+ border-color: var(--ide-highlight-background);
}
}
.ide-status-bar {
- background-color: $footer-background;
+ background-color: var(--ide-footer-background);
}
input[type='text'],
input[type='search'],
.filtered-search-box {
- border-color: $input-border;
- background: $input-background !important;
+ border-color: var(--ide-input-border);
+ background: var(--ide-input-background) !important;
}
input[type='text'],
input[type='search'],
.filtered-search-box,
textarea {
- color: $input-color !important;
+ color: var(--ide-input-color) !important;
}
.filtered-search-box input[type='search'] {
@@ -282,46 +240,49 @@
.filtered-search-token .value-container,
.filtered-search-term .value-container {
- background-color: $dropdown-hover-background;
-
- color: $text-color;
+ background-color: var(--ide-dropdown-hover-background);
+ color: var(--ide-text-color);
&:hover {
- background-color: $input-border;
+ background-color: var(--ide-input-border);
}
}
.ide-entry-dropdown-toggle:hover {
- background: $gray-800;
+ background: var(--ide-file-row-btn-hover-background);
+ }
+
+ @function calc-btn-hover-padding($original-padding, $original-border: 1px) {
+ @return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width));
}
.btn:not(.btn-link):not([disabled]):hover {
- border-width: 2px;
- padding: 5px 9px;
+ border-width: var(--ide-btn-hover-border-width);
+ padding: calc-btn-hover-padding(6px) calc-btn-hover-padding(10px);
}
.btn:not([disabled]).btn-sm:hover {
- padding: 3px 9px;
+ padding: calc-btn-hover-padding(4px) calc-btn-hover-padding(10px);
}
.btn:not([disabled]).btn-block:hover {
- padding: 5px 0;
+ padding: calc-btn-hover-padding(6px) 0;
}
.btn-inverted,
.btn-default,
.dropdown,
.dropdown-menu-toggle {
- background-color: $input-background !important;
- color: $input-color !important;
- border-color: $btn-default-border;
+ background-color: var(--ide-input-background) !important;
+ color: var(--ide-input-color) !important;
+ border-color: var(--ide-btn-default-border);
}
.btn-inverted,
.btn-default {
&:hover,
&:focus {
- border-color: $btn-default-hover-border !important;
+ border-color: var(--ide-btn-default-hover-border) !important;
}
}
@@ -329,35 +290,35 @@
.dropdown-menu-toggle {
&:hover,
&:focus {
- background-color: $gray-900 !important;
- border-color: $gray-200 !important;
+ background-color: var(--ide-dropdown-btn-hover-background) !important;
+ border-color: var(--ide-dropdown-btn-hover-border) !important;
}
}
.dropdown-menu {
- color: $text-color;
- border-color: $background;
- background-color: $dropdown-background;
+ color: var(--ide-text-color);
+ border-color: var(--ide-background);
+ background-color: var(--ide-dropdown-background);
.divider,
.nav-links:not(.quick-links) {
- background-color: $dropdown-hover-background;
- border-color: $dropdown-hover-background;
+ background-color: var(--ide-dropdown-hover-background);
+ border-color: var(--ide-dropdown-hover-background);
}
.nav-links li a.active {
- border-color: $highlight-accent;
+ border-color: var(--ide-highlight-accent);
}
.ide-nav-form .nav-links li a:not(.active) {
- background-color: $dropdown-background;
+ background-color: var(--ide-dropdown-background);
}
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a {
- color: $text-color;
+ color: var(--ide-text-color);
&.active {
- color: $text-color;
+ color: var(--ide-text-color);
}
}
@@ -366,73 +327,75 @@
li button:not(.disable-hover):hover,
li button:not(.disable-hover):focus,
li button.is-focused {
- background-color: $dropdown-hover-background;
- color: $text-color;
+ background-color: var(--ide-dropdown-hover-background);
+ color: var(--ide-text-color);
}
}
.dropdown-title,
.dropdown-input {
- border-color: $dropdown-hover-background !important;
+ border-color: var(--ide-dropdown-hover-background) !important;
}
- .btn-primary {
- background-color: $btn-primary-background;
- border-color: $btn-primary-border !important;
+ .btn-primary,
+ .btn-info {
+ background-color: var(--ide-btn-primary-background);
+ border-color: var(--ide-btn-primary-border) !important;
&:hover,
&:focus {
- border-color: $btn-primary-hover-border !important;
+ border-color: var(--ide-btn-primary-hover-border) !important;
}
}
.btn-success {
- background-color: $btn-success-background;
- border-color: $btn-success-border !important;
+ background-color: var(--ide-btn-success-background);
+ border-color: var(--ide-btn-success-border) !important;
&:hover,
&:focus {
- border-color: $btn-success-hover-border !important;
+ border-color: var(--ide-btn-success-hover-border) !important;
}
}
.btn[disabled] {
- background: $btn-default-background !important;
- border: 1px solid $btn-disabled-border !important;
- color: $btn-disabled-color !important;
+ background: var(--ide-btn-default-background) !important;
+ border: 1px solid var(--ide-btn-disabled-border) !important;
+ color: var(--ide-btn-disabled-color) !important;
}
.md-previewer,
+ pre code,
.md table:not(.code) tbody,
.ide-empty-state {
- background-color: $border-color;
+ background-color: var(--ide-border-color);
}
.ide-tree-header svg:focus,
.ide-tree-header svg:hover {
- color: $blue-600;
+ color: var(--ide-link-color);
}
.animation-container {
[class^='skeleton-line-'] {
- background-color: $gray-800;
+ background-color: var(--ide-animation-gradient-1);
&::after {
background-image: linear-gradient(to right,
- $gray-800 0%,
- $gray-700 20%,
- $gray-800 40%,
- $gray-800 100%);
+ var(--ide-animation-gradient-1) 0%,
+ var(--ide-animation-gradient-2) 20%,
+ var(--ide-animation-gradient-1) 40%,
+ var(--ide-animation-gradient-1) 100%);
}
}
}
.idiff.addition {
- background-color: $diff-insert;
+ background-color: var(--ide-diff-insert);
}
.idiff.deletion {
- background-color: $diff-remove;
+ background-color: var(--ide-diff-remove);
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index d0660422f7e..cb41d960307 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -2,8 +2,9 @@
@import 'framework/mixins';
@import './ide_mixins';
@import './ide_monaco_overrides';
+@import './ide_theme_overrides';
-@import './themes/dark';
+@import './ide_themes/dark';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
new file mode 100644
index 00000000000..809abc42a69
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
@@ -0,0 +1,45 @@
+.ide.theme-dark {
+ --ide-border-color: #1d1f21;
+ --ide-highlight-accent: #fff;
+ --ide-text-color: #ccc;
+ --ide-background: #333;
+ --ide-background-hover: #2d2d2d;
+ --ide-highlight-background: #252526;
+ --ide-link-color: #428fdc;
+ --ide-footer-background: #060606;
+
+ --ide-input-border: #868686;
+ --ide-input-background: transparent;
+ --ide-input-color: #fff;
+
+ --ide-btn-default-background: transparent;
+ --ide-btn-default-border: #bfbfbf;
+ --ide-btn-default-hover-border: #d8d8d8;
+
+ --ide-btn-primary-background: #1068bf;
+ --ide-btn-primary-border: #428fdc;
+ --ide-btn-primary-hover-border: #63a6e9;
+
+ --ide-btn-success-background: #217645;
+ --ide-btn-success-border: #108548;
+ --ide-btn-success-hover-border: #2da160;
+
+ --ide-btn-disabled-border: rgba(223, 223, 223, 0.24);
+ --ide-btn-disabled-color: rgba(145, 145, 145, 0.48);
+
+ --ide-btn-hover-border-width: 2px;
+
+ --ide-dropdown-background: #404040;
+ --ide-dropdown-hover-background: #525252;
+
+ --ide-dropdown-btn-hover-border: #{$gray-200};
+ --ide-dropdown-btn-hover-background: #{$gray-900};
+
+ --ide-file-row-btn-hover-background: #{$gray-800};
+
+ --ide-diff-insert: rgba(155, 185, 85, 0.2);
+ --ide-diff-remove: rgba(255, 0, 0, 0.2);
+
+ --ide-animation-gradient-1: #{$gray-800};
+ --ide-animation-gradient-2: #{$gray-700};
+}
diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb
index fa79f3bc4e6..58715fda152 100644
--- a/app/controllers/concerns/metrics_dashboard.rb
+++ b/app/controllers/concerns/metrics_dashboard.rb
@@ -35,10 +35,10 @@ module MetricsDashboard
private
def all_dashboards
- dashboards = dashboard_finder.find_all_paths(project_for_dashboard)
- dashboards.map do |dashboard|
- amend_dashboard(dashboard)
- end
+ dashboard_finder
+ .find_all_paths(project_for_dashboard)
+ .map(&method(:amend_dashboard))
+ .sort_by { |dashboard| [dashboard[:starred] ? 0 : 1, dashboard[:display_name].downcase] }
end
def amend_dashboard(dashboard)
@@ -46,6 +46,8 @@ module MetricsDashboard
dashboard[:can_edit] = project_dashboard ? can_edit?(dashboard) : false
dashboard[:project_blob_path] = project_dashboard ? dashboard_project_blob_path(dashboard) : nil
+ dashboard[:starred] = starred_dashboards.include?(dashboard[:path])
+ dashboard[:user_starred_path] = nil # placeholder attribute until API endpoint will be merged https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31316
dashboard
end
@@ -73,6 +75,20 @@ module MetricsDashboard
::Gitlab::Metrics::Dashboard::Finder
end
+ def starred_dashboards
+ @starred_dashboards ||= begin
+ if project_for_dashboard.present?
+ ::Metrics::UsersStarredDashboardsFinder
+ .new(user: current_user, project: project_for_dashboard)
+ .execute
+ .map(&:dashboard_path)
+ .to_set
+ else
+ Set.new
+ end
+ end
+ end
+
# Project is not defined for group and admin level clusters.
def project_for_dashboard
defined?(project) ? project : nil
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 70724394ef5..5f4d88c57e9 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -9,7 +9,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
authorize_metrics_dashboard!
push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:metrics_dashboard_annotations, project)
end
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb
index ac6a5412956..e73a591378a 100644
--- a/app/graphql/mutations/alert_management/update_alert_status.rb
+++ b/app/graphql/mutations/alert_management/update_alert_status.rb
@@ -11,7 +11,6 @@ module Mutations
def resolve(args)
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
-
result = update_status(alert, args[:status])
prepare_response(result)
@@ -20,7 +19,9 @@ module Mutations
private
def update_status(alert, status)
- ::AlertManagement::UpdateAlertStatusService.new(alert, status).execute
+ ::AlertManagement::UpdateAlertStatusService
+ .new(alert, current_user, status)
+ .execute
end
def prepare_response(result)
diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
index 068323a3073..2dd224bb17b 100644
--- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
@@ -18,7 +18,6 @@ module Resolvers
def resolve(**args)
return [] unless dashboard
- return [] unless Feature.enabled?(:metrics_dashboard_annotations, dashboard.environment&.project)
::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute
end
diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb
index e7d09866bb5..d684533ff94 100644
--- a/app/graphql/types/metrics/dashboard_type.rb
+++ b/app/graphql/types/metrics/dashboard_type.rb
@@ -11,8 +11,7 @@ module Types
description: 'Path to a file with the dashboard definition'
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
- description: 'Annotations added to the dashboard. Will always return `null` ' \
- 'if `metrics_dashboard_annotations` feature flag is disabled',
+ description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/models/group.rb b/app/models/group.rb
index bc57ab522da..37dfe27e834 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -335,6 +335,11 @@ class Group < Namespace
.where(source_id: source_ids)
end
+ def members_from_self_and_ancestors_with_effective_access_level
+ members_with_parents.select([:user_id, 'MAX(access_level) AS access_level'])
+ .group(:user_id)
+ end
+
def members_with_descendants
GroupMember
.active_without_invites_and_requests
diff --git a/app/models/member.rb b/app/models/member.rb
index 5b33333aa23..791073da095 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Member < ApplicationRecord
+ include EachBatch
include AfterCommitQueue
include Sortable
include Importable
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index def98e23828..8c2b3a65d57 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -22,16 +22,16 @@ class DiffFileBaseEntity < Grape::Entity
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
- options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+ next unless merge_request.merged? || merge_request.source_branch_exists?
- next unless merge_request.source_project
+ target_project, target_branch = edit_project_branch_options(merge_request)
if Feature.enabled?(:web_ide_default)
- ide_edit_path(merge_request.source_project, merge_request.source_branch, diff_file.new_path)
+ ide_edit_path(target_project, target_branch, diff_file.new_path)
else
- project_edit_blob_path(merge_request.source_project,
- tree_join(merge_request.source_branch, diff_file.new_path),
- options)
+ options = merge_request.persisted? && merge_request.source_branch_exists? && !merge_request.merged? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ project_edit_blob_path(target_project, tree_join(target_branch, diff_file.new_path), options)
end
end
@@ -61,7 +61,7 @@ class DiffFileBaseEntity < Grape::Entity
next unless diff_file.blob
if merge_request&.source_project && current_user
- can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch_exists? ? merge_request.source_branch : merge_request.target_branch)
else
false
end
@@ -113,4 +113,12 @@ class DiffFileBaseEntity < Grape::Entity
def current_user
request.current_user
end
+
+ def edit_project_branch_options(merge_request)
+ if merge_request.source_branch_exists? && !merge_request.merged?
+ [merge_request.source_project, merge_request.source_branch]
+ else
+ [merge_request.target_project, merge_request.target_branch]
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 568d0f6aa8f..fb4fbe57130 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -11,6 +11,10 @@ class DiffsEntity < Grape::Entity
merge_request&.source_branch
end
+ expose :source_branch_exists do |diffs|
+ merge_request&.source_branch_exists?
+ end
+
expose :target_branch_name do |diffs|
merge_request&.target_branch
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index 498cfe5930d..bbec107544e 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -21,7 +21,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
expose :labels, using: LabelEntity
expose :current_user, if: lambda { |_issuable| current_user } do
- expose :current_user, merge: true, using: API::Entities::UserBasic
+ expose :current_user, merge: true, using: ::API::Entities::UserBasic
expose :todo, using: IssuableSidebarTodoEntity do |issuable|
current_user.pending_todo_for(issuable)
diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index 0e1fcc58d7a..77f2e34fa5d 100644
--- a/app/serializers/issuable_sidebar_extras_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -21,5 +21,5 @@ class IssuableSidebarExtrasEntity < Grape::Entity
issuable.subscribed?(request.current_user, issuable.project)
end
- expose :assignees, using: API::Entities::UserBasic
+ expose :assignees, using: ::API::Entities::UserBasic
end
diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb
index 6849c62e759..b7ef7449270 100644
--- a/app/serializers/merge_request_assignee_entity.rb
+++ b/app/serializers/merge_request_assignee_entity.rb
@@ -5,3 +5,5 @@ class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
options[:merge_request]&.can_be_merged_by?(assignee)
end
end
+
+MergeRequestAssigneeEntity.prepend_if_ee('EE::MergeRequestAssigneeEntity')
diff --git a/app/services/alert_management/update_alert_status_service.rb b/app/services/alert_management/update_alert_status_service.rb
index 37960f510ef..a7ebddb82e0 100644
--- a/app/services/alert_management/update_alert_status_service.rb
+++ b/app/services/alert_management/update_alert_status_service.rb
@@ -5,14 +5,17 @@ module AlertManagement
include Gitlab::Utils::StrongMemoize
# @param alert [AlertManagement::Alert]
+ # @param user [User]
# @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES
- def initialize(alert, status)
+ def initialize(alert, user, status)
@alert = alert
+ @user = user
@status = status
end
def execute
- return error('Invalid status') unless status_key
+ return error_no_permissions unless allowed?
+ return error_invalid_status unless status_key
if alert.update(status_event: status_event)
success
@@ -23,7 +26,13 @@ module AlertManagement
private
- attr_reader :alert, :status
+ attr_reader :alert, :user, :status
+
+ delegate :project, to: :alert
+
+ def allowed?
+ user.can?(:update_alert_management_alert, project)
+ end
def status_key
strong_memoize(:status_key) do
@@ -39,6 +48,14 @@ module AlertManagement
ServiceResponse.success(payload: { alert: alert })
end
+ def error_no_permissions
+ error(_('You have no permissions'))
+ end
+
+ def error_invalid_status
+ error(_('Invalid status'))
+ end
+
def error(message)
ServiceResponse.error(payload: { alert: alert }, message: message)
end
diff --git a/app/services/authorized_project_update/project_create_service.rb b/app/services/authorized_project_update/project_create_service.rb
new file mode 100644
index 00000000000..c17c0a033fe
--- /dev/null
+++ b/app/services/authorized_project_update/project_create_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectCreateService < BaseService
+ BATCH_SIZE = 1000
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ group = project.group
+
+ unless group
+ return ServiceResponse.error(message: 'Project does not have a group')
+ end
+
+ group.members_from_self_and_ancestors_with_effective_access_level
+ .each_batch(of: BATCH_SIZE, column: :user_id) do |members|
+ attributes = members.map do |member|
+ { user_id: member.user_id, project_id: project.id, access_level: member.access_level }
+ end
+
+ ProjectAuthorization.insert_all(attributes)
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :project
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 434f584ab33..33a20afc0ba 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3,6 +3,13 @@
#
# Do not edit it manually!
---
+- :name: authorized_project_update:authorized_project_update_project_create
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
- :name: auto_devops:auto_devops_disable
:feature_category: :auto_devops
:has_external_dependencies:
diff --git a/app/workers/authorized_project_update/project_create_worker.rb b/app/workers/authorized_project_update/project_create_worker.rb
new file mode 100644
index 00000000000..651849b57ec
--- /dev/null
+++ b/app/workers/authorized_project_update/project_create_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectCreateWorker
+ include ApplicationWorker
+
+ feature_category :authentication_and_authorization
+ urgency :low
+ queue_namespace :authorized_project_update
+
+ idempotent!
+
+ def perform(project_id)
+ project = Project.find(project_id)
+
+ AuthorizedProjectUpdate::ProjectCreateService.new(project).execute
+ end
+ end
+end
diff --git a/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml
new file mode 100644
index 00000000000..843088f88a1
--- /dev/null
+++ b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml
@@ -0,0 +1,5 @@
+---
+title: Fix GitLab CI/CD Scala template
+merge_request: 30667
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-build-template-to-rules-syntax.yml b/changelogs/unreleased/move-build-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..a6681e70e23
--- /dev/null
+++ b/changelogs/unreleased/move-build-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Build.gitlab-ci.yml to `rules` syntax
+merge_request: 30895
+author:
+type: changed
diff --git a/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..dbc7e3c7e8f
--- /dev/null
+++ b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Code-Quality.gitlab-ci.yml to `rules` syntax
+merge_request: 30896
+author:
+type: changed
diff --git a/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..46ec2bcbcf5
--- /dev/null
+++ b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Deploy.gitlab-ci.yml to `rules` syntax
+merge_request: 31290
+author:
+type: changed
diff --git a/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml
new file mode 100644
index 00000000000..db9635aaac7
--- /dev/null
+++ b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Display metrics dashboards starred by user at the top of dashboard select field.
+merge_request: 31059
+author:
+type: added
diff --git a/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml
new file mode 100644
index 00000000000..84f8dfb0798
--- /dev/null
+++ b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml
@@ -0,0 +1,6 @@
+---
+title: Add metrics dashboard annotations feature, which enables marking interesting
+ events over metrics dashboard charts
+merge_request: 30371
+author:
+type: added
diff --git a/changelogs/unreleased/pages-1-18.yml b/changelogs/unreleased/pages-1-18.yml
new file mode 100644
index 00000000000..84b45bc8ad4
--- /dev/null
+++ b/changelogs/unreleased/pages-1-18.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Pages to 1.18.0
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml
new file mode 100644
index 00000000000..a4e754fee1d
--- /dev/null
+++ b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated container scanning report parser
+merge_request: 31294
+author:
+type: removed
diff --git a/changelogs/unreleased/revert-0ddee28a.yml b/changelogs/unreleased/revert-0ddee28a.yml
new file mode 100644
index 00000000000..909a8673a08
--- /dev/null
+++ b/changelogs/unreleased/revert-0ddee28a.yml
@@ -0,0 +1,5 @@
+---
+title: Move Auto DevOps Test.gitlab-ci.yml template to rules syntax instead of only/except
+merge_request: 30876
+author:
+type: changed
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d5a048f15aa..960a2eeae10 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -32,6 +32,8 @@
- 1
- - authorized_keys
- 2
+- - authorized_project_update
+ - 1
- - authorized_project_update_user_refresh_with_low_urgency
- 1
- - authorized_projects
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 317cd05850d..857de2f7fb6 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -6027,7 +6027,7 @@ type Metadata {
type MetricsDashboard {
"""
- Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled
+ Annotations added to the dashboard
"""
annotations(
"""
@@ -8504,7 +8504,7 @@ input RemoveProjectFromSecurityDashboardInput {
"""
ID of the project to remove from the Instance Security Dashboard
"""
- projectId: ID!
+ id: ID!
}
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 776dd968273..e51f44db2ff 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -16917,7 +16917,7 @@
"fields": [
{
"name": "annotations",
- "description": "Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled",
+ "description": "Annotations added to the dashboard",
"args": [
{
"name": "from",
@@ -24943,7 +24943,7 @@
"fields": null,
"inputFields": [
{
- "name": "projectId",
+ "name": "id",
"description": "ID of the project to remove from the Instance Security Dashboard",
"type": {
"kind": "NON_NULL",
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 4da62c95a69..1831417e48e 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -14,7 +14,7 @@ The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
``` yaml
-image: java:8
+image: openjdk:8
stages:
- test
diff --git a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png
new file mode 100644
index 00000000000..d767c159e8d
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png
index 07b41b471d4..07b41b471d4 100644
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 59aeba9d655..8776b626bec 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -67,7 +67,7 @@ NOTE: **Note:**
It may take several minutes for the download to start if your project consists
of thousands of vulnerabilities. Do not close the page until the download finishes.
-![CSV Export Button](img/project_security_dashboard_export_csv_v12.10.png)
+![CSV Export Button](img/project_security_dashboard_export_csv_v12_10.png)
## Group Security Dashboard
@@ -152,6 +152,22 @@ projects.
![Instance Security Dashboard with projects](img/instance_security_dashboard_with_projects_v12_8.png)
+### Export vulnerabilities
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213014) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
+
+You can export all your vulnerabilities as CSV by clicking the **{upload}** **Export**
+button located at top right of the **Instance Security Dashboard**. After the report
+is built, the CSV report downloads to your local machine. The report contains all
+vulnerabilities for the projects defined in the **Instance Security Dashboard**,
+as filters don't apply to the export function.
+
+NOTE: **Note:**
+It may take several minutes for the download to start if your project contains
+thousands of vulnerabilities. Do not close the page until the download finishes.
+
+![CSV Export Button](img/instance_security_dashboard_export_csv_v13_0.png)
+
## Keeping the dashboards up to date
The Security Dashboard displays information from the results of the most recent
diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb
index e063aa42855..80f3ee7b502 100644
--- a/lib/api/entities/user_basic.rb
+++ b/lib/api/entities/user_basic.rb
@@ -18,3 +18,5 @@ module API
end
end
end
+
+API::Entities::UserBasic.prepend_if_ee('EE::API::Entities::UserBasic')
diff --git a/lib/api/entities/user_path.rb b/lib/api/entities/user_path.rb
index 7d922b39654..3f007659813 100644
--- a/lib/api/entities/user_path.rb
+++ b/lib/api/entities/user_path.rb
@@ -12,3 +12,5 @@ module API
end
end
end
+
+API::Entities::UserPath.prepend_if_ee('EE::API::Entities::UserPath')
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index 432fa3ac0c9..81fee166175 100644
--- a/lib/api/metrics/dashboard/annotations.rb
+++ b/lib/api/metrics/dashboard/annotations.rb
@@ -28,8 +28,6 @@ module API
post ':id/metrics_dashboard/annotations' do
annotations_source_object = annotations_source[:class].find(params[:id])
- not_found! unless Feature.enabled?(:metrics_dashboard_annotations, annotations_source_object.project)
-
forbidden! unless can?(current_user, :create_metrics_dashboard_annotation, annotations_source_object)
create_service_params = declared(params).merge(annotations_source[:create_service_param_key] => annotations_source_object)
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 3949b87bbda..787f07521e0 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -15,6 +15,5 @@ build:
export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
fi
- /build/build.sh
- only:
- - branches
- - tags
+ rules:
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 9c4699f1f44..24e75c56a75 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -26,10 +26,7 @@ code_quality:
codequality: gl-code-quality-report.json
expire_in: 1 week
dependencies: []
- only:
- refs:
- - branches
- - tags
- except:
- variables:
- - $CODE_QUALITY_DISABLED
+ rules:
+ - if: '$CODE_QUALITY_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 9bf0d31409a..7f98f0074d8 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -18,16 +18,14 @@ review:
on_stop: stop_review
artifacts:
paths: [environment_url.txt]
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- refs:
- - master
- variables:
- - $REVIEW_DISABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
stop_review:
extends: .auto-deploy
@@ -41,18 +39,16 @@ stop_review:
name: review/$CI_COMMIT_REF_NAME
action: stop
dependencies: []
- when: manual
allow_failure: true
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- refs:
- - master
- variables:
- - $REVIEW_DISABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
+ when: manual
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
@@ -73,12 +69,12 @@ staging:
environment:
name: staging
url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$STAGING_ENABLED'
# Canaries are disabled by default, but if you want them,
# and know what the downsides are, you can enable this by setting
@@ -97,13 +93,13 @@ canary:
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- when: manual
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $CANARY_ENABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: manual
.production: &production_template
extends: .auto-deploy
@@ -126,32 +122,33 @@ canary:
production:
<<: *production_template
- only:
- refs:
- - master
- kubernetes: active
- except:
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$STAGING_ENABLED'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
production_manual:
<<: *production_template
- when: manual
allow_failure: false
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master" && $STAGING_ENABLED'
+ when: manual
+ - if: '$CI_COMMIT_BRANCH == "master" && $CANARY_ENABLED'
+ when: manual
# This job implements incremental rollout on for every push to `master`.
@@ -176,29 +173,29 @@ production_manual:
.manual_rollout_template: &manual_rollout_template
<<: *rollout_template
stage: production
- when: manual
- # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "manual"
- - $INCREMENTAL_ROLLOUT_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ # $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED'
+ when: manual
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
- when: delayed
- start_in: 5 minutes
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: delayed
+ start_in: 5 minutes
timed rollout 10%:
<<: *timed_rollout_template
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
index c6b0de44207..61808eae142 100644
--- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -16,9 +16,7 @@ test:
- export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}"
- cp -R . /tmp/app
- /bin/herokuish buildpack test
- only:
- - branches
- - tags
- except:
- variables:
- - $TEST_DISABLED
+ rules:
+ - if: '$TEST_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
index b4208ed9d7d..e081e20564a 100644
--- a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
@@ -1,7 +1,7 @@
-# Official Java image. Look for the different tagged releases at
-# https://hub.docker.com/r/library/java/tags/ . A Java image is not required
+# Official OpenJDK Java image. Look for the different tagged releases at
+# https://hub.docker.com/_/openjdk/ . A Java image is not required
# but an image with a JVM speeds up the build a bit.
-image: java:8
+image: openjdk:8
before_script:
# Enable the usage of sources over https
@@ -14,7 +14,7 @@ before_script:
- apt-get update -yqq
- apt-get install sbt -yqq
# Log the sbt version
- - sbt sbt-version
+ - sbt sbtVersion
test:
script:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 133c5a3659d..50a78658c28 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1721,6 +1721,9 @@ msgstr ""
msgid "AlertManagement|Create issue"
msgstr ""
+msgid "AlertManagement|Critical"
+msgstr ""
+
msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
msgstr ""
@@ -1739,6 +1742,18 @@ msgstr ""
msgid "AlertManagement|Full Alert Details"
msgstr ""
+msgid "AlertManagement|High"
+msgstr ""
+
+msgid "AlertManagement|Info"
+msgstr ""
+
+msgid "AlertManagement|Low"
+msgstr ""
+
+msgid "AlertManagement|Medium"
+msgstr ""
+
msgid "AlertManagement|More information"
msgstr ""
@@ -1778,6 +1793,9 @@ msgstr ""
msgid "AlertManagement|Triggered"
msgstr ""
+msgid "AlertManagement|Unknown"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -3545,6 +3563,9 @@ msgstr ""
msgid "Can override approvers and approvals required per merge request"
msgstr ""
+msgid "Can't edit as source branch was deleted"
+msgstr ""
+
msgid "Can't find HEAD commit for this branch"
msgstr ""
@@ -11615,6 +11636,9 @@ msgstr ""
msgid "Invalid start or end time format"
msgstr ""
+msgid "Invalid status"
+msgstr ""
+
msgid "Invalid two-factor code."
msgstr ""
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
index 4e42171e3d3..3a6a037ac9a 100644
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -114,6 +114,35 @@ describe MetricsDashboard do
end
end
end
+
+ context 'starred dashboards' do
+ let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let_it_be(:dashboards) do
+ {
+ '.gitlab/dashboards/test.yml' => dashboard_yml,
+ '.gitlab/dashboards/anomaly.yml' => dashboard_yml,
+ '.gitlab/dashboards/errors.yml' => dashboard_yml
+ }
+ end
+ let_it_be(:project) { create(:project, :custom_repo, files: dashboards) }
+
+ before do
+ create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/errors.yml')
+ create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/test.yml')
+ end
+
+ it 'adds starred dashboard information and sorts the list' do
+ all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') }
+ expected_response = [
+ { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => nil },
+ { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => nil },
+ { "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => nil },
+ { "display_name" => "Default", "starred" => false, 'user_starred_path' => nil }
+ ]
+
+ expect(all_dashboards).to eql expected_response
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index a22dc77997b..17d7e710614 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -825,7 +825,7 @@ describe Projects::IssuesController do
update_issue(issue_params: { assignee_ids: [assignee.id] })
expect(json_response['assignees'].first.keys)
- .to match_array(%w(id name username avatar_url state web_url))
+ .to include(*%w(id name username avatar_url state web_url))
end
end
@@ -1408,6 +1408,7 @@ describe Projects::IssuesController do
it 'render merge request as json' do
create_merge_request
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('merge_request')
end
diff --git a/spec/factories/alert_management/alerts.rb b/spec/factories/alert_management/alerts.rb
index 28cfe5d6b29..d98fed4a6b1 100644
--- a/spec/factories/alert_management/alerts.rb
+++ b/spec/factories/alert_management/alerts.rb
@@ -3,6 +3,7 @@ require 'ffaker'
FactoryBot.define do
factory :alert_management_alert, class: 'AlertManagement::Alert' do
+ triggered
project
title { FFaker::Lorem.sentence }
started_at { Time.current }
@@ -35,6 +36,11 @@ FactoryBot.define do
ended_at { nil }
end
+ trait :triggered do
+ status { AlertManagement::Alert::STATUSES[:triggered] }
+ without_ended_at
+ end
+
trait :acknowledged do
status { AlertManagement::Alert::STATUSES[:acknowledged] }
without_ended_at
diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json
index 9d7ca62435e..21d8efe0b2b 100644
--- a/spec/fixtures/api/schemas/entities/discussion.json
+++ b/spec/fixtures/api/schemas/entities/discussion.json
@@ -29,8 +29,15 @@
"web_url": { "type": "uri" },
"status_tooltip_html": { "type": ["string", "null"] },
"path": { "type": "string" }
- },
- "additionalProperties": false
+ },
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "path",
+ "name",
+ "username"
+ ]
},
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/entities/note_user_entity.json b/spec/fixtures/api/schemas/entities/note_user_entity.json
index 9b838054563..4a27d885cdc 100644
--- a/spec/fixtures/api/schemas/entities/note_user_entity.json
+++ b/spec/fixtures/api/schemas/entities/note_user_entity.json
@@ -16,6 +16,5 @@
"name": { "type": "string" },
"username": { "type": "string" },
"status_tooltip_html": { "$ref": "../types/nullable_string.json" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json
index 82d80b75cef..3252a37c82a 100644
--- a/spec/fixtures/api/schemas/entities/user.json
+++ b/spec/fixtures/api/schemas/entities/user.json
@@ -18,6 +18,5 @@
"name": { "type": "string" },
"username": { "type": "string" },
"status_tooltip_html": { "$ref": "../types/nullable_string.json" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index 690c4a7d4e8..d01801a15fa 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -30,7 +30,9 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required": [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
},
"variables": {
"type": "array",
diff --git a/spec/fixtures/api/schemas/public_api/v4/issue.json b/spec/fixtures/api/schemas/public_api/v4/issue.json
index bf1b4a06f0b..69ecba8b6f3 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issue.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issue.json
@@ -71,7 +71,14 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "name",
+ "username",
+ "web_url"
+ ]
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/members.json b/spec/fixtures/api/schemas/public_api/v4/members.json
index 38ad64ad061..695f00b0040 100644
--- a/spec/fixtures/api/schemas/public_api/v4/members.json
+++ b/spec/fixtures/api/schemas/public_api/v4/members.json
@@ -15,8 +15,7 @@
},
"required": [
"id", "name", "username", "state",
- "web_url", "access_level", "expires_at"
- ],
- "additionalProperties": false
+ "web_url", "access_level", "expires_at", "avatar_url"
+ ]
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index d15d2e90b05..683dcb19836 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -17,7 +17,9 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required" : [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
},
"commands_changes": { "type": "object", "additionalProperties": true },
"created_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json
index ddddd46f5c4..7baa24a6f1f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/snippets.json
+++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json
@@ -23,7 +23,9 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required" : [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
}
},
"required": [
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
index 55ac62eccdf..fa32707ead5 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -1,11 +1,16 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import AlertDetails from '~/alert_management/components/alert_details.vue';
describe('AlertDetails', () => {
let wrapper;
const newIssuePath = 'root/alerts/-/issues/new';
- function mountComponent(alert = {}, createIssueFromAlertEnabled = false) {
+ function mountComponent({
+ alert = {},
+ createIssueFromAlertEnabled = false,
+ loading = false,
+ } = {}) {
wrapper = shallowMount(AlertDetails, {
propsData: {
alertId: 'alertId',
@@ -18,6 +23,15 @@ describe('AlertDetails', () => {
provide: {
glFeatures: { createIssueFromAlertEnabled },
},
+ mocks: {
+ $apollo: {
+ queries: {
+ alert: {
+ loading,
+ },
+ },
+ },
+ },
});
}
@@ -32,17 +46,11 @@ describe('AlertDetails', () => {
describe('Alert details', () => {
describe('when alert is null', () => {
beforeEach(() => {
- mountComponent(null);
+ mountComponent({ alert: null });
});
- describe('when alert is null', () => {
- beforeEach(() => {
- mountComponent(null);
- });
-
- it('shows an empty state', () => {
- expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false);
- });
+ it('shows an empty state', () => {
+ expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false);
});
});
@@ -71,7 +79,7 @@ describe('AlertDetails', () => {
describe('Create issue from alert', () => {
describe('createIssueFromAlertEnabled feature flag enabled', () => {
it('should display a button that links to new issue page', () => {
- mountComponent({}, true);
+ mountComponent({ createIssueFromAlertEnabled: true });
expect(findCreatedIssueBtn().exists()).toBe(true);
expect(findCreatedIssueBtn().attributes('href')).toBe(newIssuePath);
});
@@ -79,10 +87,20 @@ describe('AlertDetails', () => {
describe('createIssueFromAlertEnabled feature flag disabled', () => {
it('should display a button that links to a new issue page', () => {
- mountComponent({}, false);
+ mountComponent({ createIssueFromAlertEnabled: false });
expect(findCreatedIssueBtn().exists()).toBe(false);
});
});
});
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mountComponent({ loading: true });
+ });
+
+ it('displays a loading state when loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index d7170e71a96..81d966a42b5 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -26,6 +26,7 @@ describe('AlertManagementList', () => {
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
+ const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
function mountComponent({
props = {
@@ -201,6 +202,20 @@ describe('AlertManagementList', () => {
});
});
+ it('Internationalizes severity text', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+
+ expect(
+ findSeverityFields()
+ .at(0)
+ .text(),
+ ).toBe('Critical');
+ });
+
describe('handle date fields', () => {
it('should display time ago dates when values provided', () => {
mountComponent({
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json
index d4667eb21f8..e0b8fa55507 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/alert_management/mocks/alerts.json
@@ -2,7 +2,7 @@
{
"iid": "1527542",
"title": "SyntaxError: Invalid or unexpected token",
- "severity": "Critical",
+ "severity": "CRITICAL",
"eventCount": 7,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
@@ -11,7 +11,7 @@
{
"iid": "1527542",
"title": "Some otherr alert Some otherr alert Some otherr alert Some otherr alert Some otherr alert Some otherr alert",
- "severity": "Medium",
+ "severity": "MEDIUM",
"eventCount": 1,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
@@ -20,7 +20,7 @@
{
"iid": "1527542",
"title": "SyntaxError: Invalid or unexpected token",
- "severity": "Low",
+ "severity": "LOW",
"eventCount": 4,
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js
index f9a1d4a84a8..71512c1c4af 100644
--- a/spec/frontend/diffs/components/edit_button_spec.js
+++ b/spec/frontend/diffs/components/edit_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlDeprecatedButton } from '@gitlab/ui';
import EditButton from '~/diffs/components/edit_button.vue';
const editPath = 'test-path';
@@ -22,7 +23,7 @@ describe('EditButton', () => {
canCurrentUserFork: false,
});
- expect(wrapper.attributes('href')).toBe(editPath);
+ expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(editPath);
});
it('emits a show fork message event if current user can fork', () => {
@@ -30,7 +31,7 @@ describe('EditButton', () => {
editPath,
canCurrentUserFork: true,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeTruthy();
@@ -42,7 +43,7 @@ describe('EditButton', () => {
editPath,
canCurrentUserFork: false,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeFalsy();
@@ -55,10 +56,20 @@ describe('EditButton', () => {
canCurrentUserFork: true,
canModifyBlob: true,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeFalsy();
});
});
+
+ it('disables button if editPath is empty', () => {
+ createComponent({
+ editPath: '',
+ canCurrentUserFork: true,
+ canModifyBlob: true,
+ });
+
+ expect(wrapper.find(GlDeprecatedButton).attributes('disabled')).toBe('true');
+ });
});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index d9794c34b3b..901b698b703 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -97,7 +97,11 @@ describe('Monitoring store actions', () => {
null,
state,
[],
- [{ type: 'fetchEnvironmentsData' }, { type: 'fetchDashboard' }],
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
);
});
diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
index 126362d024a..8b9abd9497d 100644
--- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
+++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Mutations::AlertManagement::UpdateAlertStatus do
let_it_be(:current_user) { create(:user) }
- let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') }
+ let_it_be(:alert) { create(:alert_management_alert, :triggered) }
let_it_be(:project) { alert.project }
let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value }
let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } }
@@ -53,7 +53,7 @@ describe Mutations::AlertManagement::UpdateAlertStatus do
it 'returns the alert with errors' do
expect(resolve).to eq(
alert: alert,
- errors: ['Invalid status']
+ errors: [_('Invalid status')]
)
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..b2a9e3f5cf4
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Build.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the build job' do
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the build job' do
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the build job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..9c5b2fd5099
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Code-Quality.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Code-Quality') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the code_quality job' do
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the code_quality job' do
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the code_quality job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+
+ context 'CODE_QUALITY_DISABLED is set' do
+ before do
+ create(:ci_variable, key: 'CODE_QUALITY_DISABLED', value: 'true', project: project)
+ end
+
+ context 'on master' do
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..a6ae23c85d3
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Deploy.gitlab-ci.yml' do
+ subject(:template) do
+ <<~YAML
+ stages:
+ - test
+ - review
+ - staging
+ - canary
+ - production
+ - incremental rollout 10%
+ - incremental rollout 25%
+ - incremental rollout 50%
+ - incremental rollout 100%
+ - cleanup
+
+ include:
+ - template: Jobs/Deploy.gitlab-ci.yml
+
+ placeholder:
+ script:
+ - echo "Ensure at least one job to keep pipeline validator happy"
+ YAML
+ end
+
+ describe 'the created pipeline' do
+ let(:user) { create(:admin) }
+ let(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template)
+
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'with no cluster' do
+ it 'does not create any kubernetes deployment jobs' do
+ expect(build_names).to eq %w(placeholder)
+ end
+ end
+
+ context 'with only a disabled cluster' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) }
+
+ it 'does not create any kubernetes deployment jobs' do
+ expect(build_names).to eq %w(placeholder)
+ end
+ end
+
+ context 'with an active cluster' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+
+ context 'on master' do
+ it 'by default' do
+ expect(build_names).to include('production')
+ expect(build_names).not_to include('review')
+ end
+
+ it 'when CANARY_ENABLED' do
+ create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true')
+
+ expect(build_names).to include('production_manual')
+ expect(build_names).to include('canary')
+ expect(build_names).not_to include('production')
+ end
+
+ it 'when STAGING_ENABLED' do
+ create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: 'true')
+
+ expect(build_names).to include('production_manual')
+ expect(build_names).to include('staging')
+ expect(build_names).not_to include('production')
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_MODE == timed' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ expect(build_names).to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_ENABLED' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ expect(build_names).to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_MODE == manual' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ expect(build_names).to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ end
+ end
+
+ shared_examples_for 'review app deployment' do
+ it 'creates the review and stop_review jobs but no production jobs' do
+ expect(build_names).to include('review')
+ expect(build_names).to include('stop_review')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('staging')
+ expect(build_names).not_to include('canary')
+ expect(build_names).not_to include('timed rollout 10%')
+ expect(build_names).not_to include('timed rollout 25%')
+ expect(build_names).not_to include('timed rollout 50%')
+ expect(build_names).not_to include('timed rollout 100%')
+ expect(build_names).not_to include('rollout 10%')
+ expect(build_names).not_to include('rollout 25%')
+ expect(build_names).not_to include('rollout 50%')
+ expect(build_names).not_to include('rollout 100%')
+ end
+
+ it 'does not include review when REVIEW_DISABLED' do
+ create(:ci_variable, project: project, key: 'REVIEW_DISABLED', value: 'true')
+
+ expect(build_names).not_to include('review')
+ expect(build_names).not_to include('stop_review')
+ end
+ end
+
+ context 'on branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ before do
+ allow_any_instance_of(Gitlab::Ci::Pipeline::Chain::Validate::Repository).to receive(:perform!).and_return(true)
+ end
+
+ it_behaves_like 'review app deployment'
+
+ context 'when INCREMENTAL_ROLLOUT_ENABLED' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'when INCREMENTAL_ROLLOUT_MODE == "timed"' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'when INCREMENTAL_ROLLOUT_MODE == "manual"' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..2186bf038eb
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Test.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Test') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the test job' do
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the test job' do
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the test job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+
+ context 'TEST_DISABLED is set' do
+ before do
+ create(:ci_variable, key: 'TEST_DISABLED', value: 'true', project: project)
+ end
+
+ context 'on master' do
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 0c5d172f17c..e98f9fe1b04 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -40,11 +40,7 @@ describe 'Auto-DevOps.gitlab-ci.yml' do
end
context 'when the project has an active cluster' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
-
- before do
- allow(cluster).to receive(:active?).and_return(true)
- end
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
describe 'deployment-related builds' do
context 'on default branch' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5ac79807c78..8fa17026934 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -659,6 +659,42 @@ describe Group do
end
end
+ describe '#members_from_self_and_ancestors_with_effective_access_level' do
+ let!(:group_parent) { create(:group, :private) }
+ let!(:group) { create(:group, :private, parent: group_parent) }
+ let!(:group_child) { create(:group, :private, parent: group) }
+
+ let!(:user) { create(:user) }
+
+ let(:parent_group_access_level) { Gitlab::Access::REPORTER }
+ let(:group_access_level) { Gitlab::Access::DEVELOPER }
+ let(:child_group_access_level) { Gitlab::Access::MAINTAINER }
+
+ before do
+ create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level)
+ create(:group_member, user: user, group: group, access_level: group_access_level)
+ create(:group_member, user: user, group: group_child, access_level: child_group_access_level)
+ end
+
+ it 'returns effective access level for user' do
+ expect(group_parent.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => parent_group_access_level)
+ )
+ )
+ expect(group.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => group_access_level)
+ )
+ )
+ expect(group_child.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => child_group_access_level)
+ )
+ )
+ end
+ end
+
describe '#direct_and_indirect_members' do
let!(:group) { create(:group, :nested) }
let!(:sub_group) { create(:group, parent: group) }
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index ceeaa12a2bf..cb35411b7a5 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -21,6 +21,7 @@ describe 'Getting Metrics Dashboard Annotations' do
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
+ let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)}
@@ -47,63 +48,40 @@ describe 'Getting Metrics Dashboard Annotations' do
)
end
- context 'feature flag metrics_dashboard_annotations' do
- let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
- before do
- project.add_developer(current_user)
- end
+ it_behaves_like 'a working graphql query'
- context 'is off' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: false)
- post_graphql(query, current_user: current_user)
- end
+ it 'returns annotations' do
+ annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
- it 'returns empty nodes array' do
- annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
+ expect(annotations).to match_array [{
+ "description" => annotation.description,
+ "id" => annotation.to_global_id.to_s,
+ "panelId" => annotation.panel_xid,
+ "startingAt" => annotation.starting_at.iso8601,
+ "endingAt" => nil
+ }]
+ end
- expect(annotations).to be_empty
- end
- end
+ context 'arguments' do
+ context 'from is missing' do
+ let(:args) { "to: \"#{from}\"" }
- context 'is on' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: true)
+ it 'returns error' do
post_graphql(query, current_user: current_user)
- end
- it_behaves_like 'a working graphql query'
-
- it 'returns annotations' do
- annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
-
- expect(annotations).to match_array [{
- "description" => annotation.description,
- "id" => annotation.to_global_id.to_s,
- "panelId" => annotation.panel_xid,
- "startingAt" => annotation.starting_at.iso8601,
- "endingAt" => nil
- }]
+ expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
end
+ end
- context 'arguments' do
- context 'from is missing' do
- let(:args) { "to: \"#{from}\"" }
-
- it 'returns error' do
- post_graphql(query, current_user: current_user)
-
- expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
- end
- end
-
- context 'to is missing' do
- let(:args) { "from: \"#{from}\"" }
+ context 'to is missing' do
+ let(:args) { "from: \"#{from}\"" }
- it_behaves_like 'a working graphql query'
- end
- end
+ it_behaves_like 'a working graphql query'
end
end
end
diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb
index c6a41ee6444..ec88b4db256 100644
--- a/spec/requests/api/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb
@@ -19,75 +19,55 @@ describe API::Metrics::Dashboard::Annotations do
end
context "with :source_type == #{source_type.pluralize}" do
- context 'feature flag metrics_dashboard_annotations' do
- context 'is on' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: { enabled: true, thing: project })
- end
+ context 'with correct permissions' do
+ context 'with valid parameters' do
+ it 'creates a new annotation', :aggregate_failures do
+ post api(url, user), params: params
- context 'with correct permissions' do
- context 'with valid parameters' do
- it 'creates a new annotation', :aggregate_failures do
- post api(url, user), params: params
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["#{source_type}_id"]).to eq(source.id)
- expect(json_response['starting_at'].to_time).to eq(starting_at.to_time)
- expect(json_response['ending_at'].to_time).to eq(ending_at.to_time)
- expect(json_response['description']).to eq(params[:description])
- expect(json_response['dashboard_path']).to eq(dashboard)
- end
- end
-
- context 'with invalid parameters' do
- it 'returns error messsage' do
- post api(url, user), params: { dashboard_path: nil, starting_at: nil, description: nil }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] })
- end
- end
-
- context 'with undeclared params' do
- before do
- params[:undeclared_param] = 'xyz'
- end
-
- it 'filters out undeclared params' do
- expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param))
-
- post api(url, user), params: params
- end
- end
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["#{source_type}_id"]).to eq(source.id)
+ expect(json_response['starting_at'].to_time).to eq(starting_at.to_time)
+ expect(json_response['ending_at'].to_time).to eq(ending_at.to_time)
+ expect(json_response['description']).to eq(params[:description])
+ expect(json_response['dashboard_path']).to eq(dashboard)
end
+ end
- context 'without correct permissions' do
- let_it_be(:guest) { create(:user) }
-
- before do
- project.add_guest(guest)
- end
-
- it 'returns error messsage' do
- post api(url, guest), params: params
+ context 'with invalid parameters' do
+ it 'returns error messsage' do
+ post api(url, user), params: { dashboard_path: nil, starting_at: nil, description: nil }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] })
end
end
- context 'is off' do
+ context 'with undeclared params' do
before do
- stub_feature_flags(metrics_dashboard_annotations: { enabled: false })
+ params[:undeclared_param] = 'xyz'
end
- it 'returns error messsage' do
- post api(url, user), params: params
+ it 'filters out undeclared params' do
+ expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param))
- expect(response).to have_gitlab_http_status(:not_found)
+ post api(url, user), params: params
end
end
end
+
+ context 'without correct permissions' do
+ let_it_be(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'returns error message' do
+ post api(url, guest), params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index f776faf6458..0deff138e2e 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1806,7 +1806,7 @@ describe API::Projects do
first_user = json_response.first
expect(first_user['username']).to eq(user.username)
expect(first_user['name']).to eq(user.name)
- expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ expect(first_user.keys).to include(*%w[name username id state avatar_url web_url])
end
end
diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb
index 80f5bc8f159..1fd697970de 100644
--- a/spec/serializers/diff_file_base_entity_spec.rb
+++ b/spec/serializers/diff_file_base_entity_spec.rb
@@ -34,4 +34,62 @@ describe DiffFileBaseEntity do
expect(entity[:new_size]).to eq(132)
end
end
+
+ context 'edit_path' do
+ let(:diff_file) { merge_request.diffs.diff_files.to_a.last }
+ let(:options) { { request: EntityRequest.new(current_user: create(:user)), merge_request: merge_request } }
+ let(:params) { {} }
+
+ before do
+ stub_feature_flags(web_ide_default: false)
+ end
+
+ shared_examples 'a diff file edit path to the source branch' do
+ it do
+ expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.source_branch, diff_file.new_path), params))
+ end
+ end
+
+ context 'open' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_branch: 'master', source_branch: 'feature') }
+ let(:params) { { from_merge_request_iid: merge_request.iid } }
+
+ it_behaves_like 'a diff file edit path to the source branch'
+
+ context 'removed source branch' do
+ before do
+ allow(merge_request).to receive(:source_branch_exists?).and_return(false)
+ end
+
+ it do
+ expect(entity[:edit_path]).to eq(nil)
+ end
+ end
+ end
+
+ context 'closed' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :closed, target_branch: 'master', source_branch: 'feature') }
+ let(:params) { { from_merge_request_iid: merge_request.iid } }
+
+ it_behaves_like 'a diff file edit path to the source branch'
+
+ context 'removed source branch' do
+ before do
+ allow(merge_request).to receive(:source_branch_exists?).and_return(false)
+ end
+
+ it do
+ expect(entity[:edit_path]).to eq(nil)
+ end
+ end
+ end
+
+ context 'merged' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
+
+ it do
+ expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.target_branch, diff_file.new_path), {}))
+ end
+ end
+ end
end
diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb
index a6bf9a7700e..3ed2b7c9452 100644
--- a/spec/serializers/diffs_metadata_entity_spec.rb
+++ b/spec/serializers/diffs_metadata_entity_spec.rb
@@ -29,7 +29,7 @@ describe DiffsMetadataEntity do
:added_lines, :removed_lines, :render_overflow_warning,
:email_patch_path, :plain_diff_path,
:merge_request_diffs, :context_commits,
- :definition_path_prefix,
+ :definition_path_prefix, :source_branch_exists,
# Attributes
:diff_files
)
diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
index b364b1a3306..b2db57801ea 100644
--- a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
+++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequestSidebarBasicEntity do
describe '#current_user' do
it 'contains attributes related to the current user' do
- expect(entity[:current_user].keys).to contain_exactly(
+ expect(entity[:current_user].keys).to include(
:id, :name, :username, :state, :avatar_url, :web_url, :todo,
:can_edit, :can_move, :can_admin_label, :can_merge
)
diff --git a/spec/services/alert_management/update_alert_status_service_spec.rb b/spec/services/alert_management/update_alert_status_service_spec.rb
index 44083128453..b287d0d1614 100644
--- a/spec/services/alert_management/update_alert_status_service_spec.rb
+++ b/spec/services/alert_management/update_alert_status_service_spec.rb
@@ -3,27 +3,64 @@
require 'spec_helper'
describe AlertManagement::UpdateAlertStatusService do
- let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') }
+ let(:project) { alert.project }
+ let_it_be(:user) { build(:user) }
+
+ let_it_be(:alert, reload: true) do
+ create(:alert_management_alert, :triggered)
+ end
+
+ let(:service) { described_class.new(alert, user, new_status) }
describe '#execute' do
- subject(:execute) { described_class.new(alert, new_status).execute }
+ shared_examples 'update failure' do |error_message|
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to eq(error_message)
+ expect(response.payload[:alert]).to eq(alert)
+ end
+
+ it 'does not update the status' do
+ expect { response }.not_to change { alert.status }
+ end
+ end
let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value }
+ let(:can_update) { true }
+
+ subject(:response) { service.execute }
+
+ before do
+ allow(user).to receive(:can?)
+ .with(:update_alert_management_alert, project)
+ .and_return(can_update)
+ end
+
+ it 'returns success' do
+ expect(response).to be_success
+ expect(response.payload[:alert]).to eq(alert)
+ end
it 'updates the status' do
- expect { execute }.to change { alert.acknowledged? }.to(true)
+ expect { response }.to change { alert.acknowledged? }.to(true)
end
- context 'with unknown status' do
- let(:new_status) { 'random_status' }
+ context 'when user has no permissions' do
+ let(:can_update) { false }
- it 'returns an error' do
- expect(execute.status).to eq(:error)
- end
+ include_examples 'update failure', _('You have no permissions')
+ end
- it 'does not update the status' do
- expect { execute }.not_to change { alert.status }
- end
+ context 'with no status' do
+ let(:new_status) { nil }
+
+ include_examples 'update failure', _('Invalid status')
+ end
+
+ context 'with unknown status' do
+ let(:new_status) { -1 }
+
+ include_examples 'update failure', _('Invalid status')
end
end
end
diff --git a/spec/services/authorized_project_update/project_create_service_spec.rb b/spec/services/authorized_project_update/project_create_service_spec.rb
new file mode 100644
index 00000000000..49ea538d909
--- /dev/null
+++ b/spec/services/authorized_project_update/project_create_service_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::ProjectCreateService do
+ let_it_be(:group_parent) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: group_parent) }
+ let_it_be(:group_child) { create(:group, :private, parent: group) }
+
+ let_it_be(:group_project) { create(:project, group: group) }
+
+ let_it_be(:parent_group_user) { create(:user) }
+ let_it_be(:group_user) { create(:user) }
+ let_it_be(:child_group_user) { create(:user) }
+
+ let(:access_level) { Gitlab::Access::MAINTAINER }
+
+ subject(:service) { described_class.new(group_project) }
+
+ describe '#perform' do
+ context 'direct group members' do
+ before do
+ create(:group_member, access_level: access_level, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'inherited group members' do
+ before do
+ create(:group_member, access_level: access_level, group: group_parent, user: parent_group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: parent_group_user.id,
+ access_level: access_level)
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'membership overrides' do
+ before do
+ create(:group_member, access_level: Gitlab::Access::REPORTER, group: group_parent, user: group_user)
+ create(:group_member, access_level: Gitlab::Access::DEVELOPER, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: Gitlab::Access::DEVELOPER)
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'no group member' do
+ it 'does not create project authorization' do
+ expect { service.execute }.not_to(
+ change { ProjectAuthorization.count }.from(0))
+ end
+ end
+
+ context 'unapproved access requests' do
+ before do
+ create(:group_member, :guest, :access_request, user: group_user, group: group)
+ end
+
+ it 'does not create project authorization' do
+ expect { service.execute }.not_to(
+ change { ProjectAuthorization.count }.from(0))
+ end
+ end
+
+ context 'project has more user than BATCH_SIZE' do
+ let(:batch_size) { 2 }
+ let(:users) { create_list(:user, batch_size + 1 ) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", batch_size)
+
+ users.each do |user|
+ create(:group_member, access_level: access_level, group: group_parent, user: user)
+ end
+
+ ProjectAuthorization.delete_all
+ end
+
+ it 'bulk creates project authorizations in batches' do
+ users.each_slice(batch_size) do |batch|
+ attributes = batch.map do |user|
+ { user_id: user.id, project_id: group_project.id, access_level: access_level }
+ end
+
+ expect(ProjectAuthorization).to(
+ receive(:insert_all).with(array_including(attributes)).and_call_original)
+ end
+
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(batch_size + 1))
+ end
+ end
+
+ context 'ignores existing project authorizations' do
+ before do
+ # ProjectAuthorizations is also created because of an after_commit
+ # callback on Member model
+ create(:group_member, access_level: access_level, group: group, user: group_user)
+ end
+
+ it 'does not create project authorization' do
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect { service.execute }.not_to(
+ change { project_authorization.reload.exists? }.from(true))
+ end
+ end
+ end
+end
diff --git a/spec/workers/authorized_project_update/project_create_worker_spec.rb b/spec/workers/authorized_project_update/project_create_worker_spec.rb
new file mode 100644
index 00000000000..5ebfb60bc79
--- /dev/null
+++ b/spec/workers/authorized_project_update/project_create_worker_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::ProjectCreateWorker do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group_project) { create(:project, group: group) }
+ let_it_be(:group_user) { create(:user) }
+
+ let(:access_level) { Gitlab::Access::MAINTAINER }
+
+ subject(:worker) { described_class.new }
+
+ it 'calls AuthorizedProjectUpdate::ProjectCreateService' do
+ expect_next_instance_of(AuthorizedProjectUpdate::ProjectCreateService) do |service|
+ expect(service).to(receive(:execute))
+ end
+
+ worker.perform(group_project.id)
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = worker.perform(group_project.id)
+
+ expect(result.success?).to be_truthy
+ end
+
+ context 'idempotence' do
+ before do
+ create(:group_member, access_level: Gitlab::Access::MAINTAINER, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { group_project.id }
+
+ it 'creates project authorization' do
+ subject
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect(project_authorization).to exist
+ expect(ProjectAuthorization.count).to eq(1)
+ end
+ end
+ end
+end