summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--.rubocop_todo/layout/space_inside_parens.yml23
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue160
-rw-r--r--app/assets/javascripts/gitlab_version_check/constants.js20
-rw-r--r--app/assets/javascripts/gitlab_version_check/index.js33
-rw-r--r--app/assets/javascripts/gitlab_version_check/utils.js18
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue24
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue5
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb48
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb1
-rw-r--r--app/graphql/types/ci/freeze_period_status_enum.rb13
-rw-r--r--app/graphql/types/ci/freeze_period_type.rb37
-rw-r--r--app/graphql/types/environment_type.rb5
-rw-r--r--app/graphql/types/work_items/widgets/hierarchy_type.rb5
-rw-r--r--app/models/ci/freeze_period.rb59
-rw-r--r--app/models/ci/freeze_period_status.rb31
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/environment.rb6
-rw-r--r--app/models/project.rb20
-rw-r--r--app/models/work_item.rb19
-rw-r--r--app/models/work_items/parent_link.rb26
-rw-r--r--app/policies/ci/freeze_period_policy.rb2
-rw-r--r--app/presenters/ci/freeze_period_presenter.rb13
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/application_settings/_default_branch.html.haml2
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml4
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/identities/_identity.html.haml4
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/groups/registry/repositories/index.html.haml2
-rw-r--r--app/views/groups/runners/index.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml26
-rw-r--r--app/views/notify/autodevops_disabled_email.html.haml2
-rw-r--r--app/views/notify/issue_moved_email.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml10
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/issues/service_desk.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/shared/_label.html.haml12
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/builds/_tabs.html.haml2
-rw-r--r--app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml1
-rw-r--r--app/views/shared/integrations/prometheus/_metrics.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml4
-rw-r--r--app/views/shared/nav/_sidebar_submenu.html.haml2
-rw-r--r--app/views/shared/projects/protected_branches/_update_protected_branch.html.haml4
-rw-r--r--config/feature_flags/development/projects_preloader_fix.yml2
-rw-r--r--config/feature_flags/development/route_hll_to_snowplow_phase4.yml8
-rw-r--r--doc/api/deployments.md23
-rw-r--r--doc/api/graphql/reference/index.md24
-rw-r--r--doc/architecture/blueprints/work_items/index.md2
-rw-r--r--doc/development/documentation/styleguide/index.md30
-rw-r--r--doc/development/documentation/versions.md4
-rw-r--r--doc/user/tasks.md5
-rw-r--r--lib/atlassian/jira_connect/jwt/asymmetric.rb6
-rw-r--r--lib/backup/files.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb2
-rw-r--r--lib/gitlab/ci/reports/security/finding.rb2
-rw-r--r--lib/gitlab/ci/runner_instructions.rb2
-rw-r--r--lib/gitlab/database/partitioning/single_numeric_list_partition.rb2
-rw-r--r--lib/gitlab/database/postgres_hll/buckets.rb2
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/import_export/decompressed_archive_size_validator.rb2
-rw-r--r--lib/gitlab/jira_import/issues_importer.rb2
-rw-r--r--lib/gitlab/memory/watchdog/monitor_state.rb2
-rw-r--r--lib/gitlab/prometheus_client.rb2
-rw-r--r--lib/gitlab/sidekiq_daemon/memory_killer.rb4
-rw-r--r--lib/gitlab/tracking/incident_management.rb2
-rw-r--r--lib/gitlab/visibility_level.rb2
-rw-r--r--lib/gitlab/work_items/work_item_hierarchy.rb48
-rw-r--r--lib/security/ci_configuration/sast_build_action.rb2
-rw-r--r--lib/tasks/gitlab/sidekiq.rake6
-rw-r--r--locale/gitlab.pot24
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js202
-rw-r--r--spec/frontend/gitlab_version_check/index_spec.js31
-rw-r--r--spec/frontend/gitlab_version_check/mock_data.js8
-rw-r--r--spec/frontend/gitlab_version_check/utils_spec.js35
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js2
-rw-r--r--spec/graphql/types/ci/freeze_period_status_enum_spec.rb9
-rw-r--r--spec/graphql/types/ci/freeze_period_type_spec.rb17
-rw-r--r--spec/graphql/types/environment_type_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb24
-rw-r--r--spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb109
-rw-r--r--spec/models/ci/freeze_period_spec.rb129
-rw-r--r--spec/models/ci/freeze_period_status_spec.rb71
-rw-r--r--spec/models/environment_spec.rb19
-rw-r--r--spec/models/project_spec.rb35
-rw-r--r--spec/models/work_item_spec.rb57
-rw-r--r--spec/models/work_items/parent_link_spec.rb49
-rw-r--r--spec/presenters/ci/freeze_period_presenter_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb5
-rw-r--r--spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb4
104 files changed, 1447 insertions, 336 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index edbbe90a774..5dfcc5715da 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,6 +25,7 @@ default:
timeout: 90m
workflow:
+ name: '$PIPELINE_NAME'
rules:
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
- if: '$FORCE_GITLAB_CI'
@@ -39,18 +40,23 @@ workflow:
- if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
variables:
RUBY_VERSION: "3.0"
+ PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
# For merge requests running exclusively in Ruby 3.0
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
variables:
RUBY_VERSION: "3.0"
+ PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
# For (detached) merge request pipelines.
- if: '$CI_MERGE_REQUEST_IID'
+ variables:
+ PIPELINE_NAME: '$CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
# For the scheduled pipelines, we set specific variables.
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"'
variables:
CRYSTALBALL: "true"
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
NOTIFY_PIPELINE_FAILURE_CHANNEL: "master-broken"
+ PIPELINE_NAME: 'Scheduled $CI_COMMIT_BRANCH pipeline'
# Run pipelines for ruby3 branch
- if: '$CI_COMMIT_BRANCH == "ruby3" && $CI_PIPELINE_SOURCE == "schedule"'
variables:
@@ -58,6 +64,7 @@ workflow:
NOTIFY_PIPELINE_FAILURE_CHANNEL: "f_ruby3"
OMNIBUS_GITLAB_RUBY3_BUILD: "true"
OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY3"
+ PIPELINE_NAME: 'Scheduled ruby 3 pipeline'
# This work around https://gitlab.com/gitlab-org/gitlab/-/issues/332411 whichs prevents usage of dependency proxy
# when pipeline is triggered by a project access token.
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $GITLAB_USER_LOGIN =~ /project_\d+_bot\d*/'
diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml
index 3e443d9d5d1..2c0d8b377e7 100644
--- a/.rubocop_todo/layout/space_inside_parens.yml
+++ b/.rubocop_todo/layout/space_inside_parens.yml
@@ -1,9 +1,7 @@
---
# Cop supports --autocorrect.
Layout/SpaceInsideParens:
- # Offense count: 422
- # Temporarily disabled due to too many offenses
- Enabled: false
+ Details: grace period
Exclude:
- 'db/post_migrate/20210722042939_update_issuable_slas_where_issue_closed.rb'
- 'ee/app/models/ee/dependency_proxy/blob.rb'
@@ -11,6 +9,7 @@ Layout/SpaceInsideParens:
- 'ee/lib/ee/gitlab/auth/ldap/access.rb'
- 'ee/lib/gitlab/auth/smartcard/session.rb'
- 'ee/lib/system_check/geo/current_node_check.rb'
+ - 'ee/spec/controllers/projects/mirrors_controller_spec.rb'
- 'ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb'
- 'ee/spec/finders/epics_finder_spec.rb'
- 'ee/spec/finders/security/findings_finder_spec.rb'
@@ -31,6 +30,7 @@ Layout/SpaceInsideParens:
- 'ee/spec/models/ee/iteration_spec.rb'
- 'ee/spec/models/ee/iterations/cadence_spec.rb'
- 'ee/spec/models/ee/key_spec.rb'
+ - 'ee/spec/models/ee/project_setting_spec.rb'
- 'ee/spec/models/ee/system_note_metadata_spec.rb'
- 'ee/spec/models/geo/every_geo_event_spec.rb'
- 'ee/spec/models/incident_management/escalation_rule_spec.rb'
@@ -87,21 +87,6 @@ Layout/SpaceInsideParens:
- 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb'
- 'ee/spec/workers/elastic/migration_worker_spec.rb'
- 'ee/spec/workers/security/auto_fix_worker_spec.rb'
- - 'lib/backup/files.rb'
- - 'lib/gitlab/ci/reports/security/finding.rb'
- - 'lib/gitlab/ci/runner_instructions.rb'
- - 'lib/gitlab/database/partitioning/single_numeric_list_partition.rb'
- - 'lib/gitlab/database/postgres_hll/buckets.rb'
- - 'lib/gitlab/diff/parser.rb'
- - 'lib/gitlab/gitaly_client/commit_service.rb'
- - 'lib/gitlab/import_export/decompressed_archive_size_validator.rb'
- - 'lib/gitlab/memory/watchdog/monitor_state.rb'
- - 'lib/gitlab/prometheus_client.rb'
- - 'lib/gitlab/sidekiq_daemon/memory_killer.rb'
- - 'lib/gitlab/tracking/incident_management.rb'
- - 'lib/gitlab/visibility_level.rb'
- - 'lib/security/ci_configuration/sast_build_action.rb'
- - 'lib/tasks/gitlab/sidekiq.rake'
- 'qa/qa/page/group/settings/group_deploy_tokens.rb'
- 'qa/qa/specs/features/ee/browser_ui/10_govern/scan_result_policy_vulnerabilities_spec.rb'
- 'qa/qa/tools/delete_subgroups.rb'
@@ -205,6 +190,8 @@ Layout/SpaceInsideParens:
- 'spec/mailers/emails/profile_spec.rb'
- 'spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb'
- 'spec/migrations/backfill_issues_upvotes_count_spec.rb'
+ - 'spec/models/ci/pending_build_spec.rb'
+ - 'spec/models/ci/running_build_spec.rb'
- 'spec/models/ml/candidate_metric_spec.rb'
- 'spec/models/ml/candidate_spec.rb'
- 'spec/policies/clusters/agent_policy_spec.rb'
diff --git a/Gemfile b/Gemfile
index 34b730da95d..ee53723abcb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -358,7 +358,7 @@ gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client'
gem 'warning', '~> 1.3.0'
group :development do
- gem 'lefthook', '~> 1.2.2', require: false
+ gem 'lefthook', '~> 1.2.4', require: false
gem 'rubocop'
gem 'solargraph', '~> 0.47.2', require: false
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 2dcf8cd9b5f..4d4d4a61329 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -312,7 +312,7 @@
{"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"},
{"name":"kubeclient","version":"4.9.3","platform":"ruby","checksum":"d5d38e719fbac44f396851aa57cd1b9f4f7dab4410ab680ccd21c9b741230046"},
{"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"},
-{"name":"lefthook","version":"1.2.2","platform":"ruby","checksum":"51359198401f1d6ccd4d21ce974eadad871d725f6de08fddb2f441512e34049f"},
+{"name":"lefthook","version":"1.2.4","platform":"ruby","checksum":"9cca2d00f9363c308219ba040da3e90dc5c059e86e43cf9a5735ef7b829801bf"},
{"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"},
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
{"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 986110378c7..92e3527e871 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -832,7 +832,7 @@ GEM
rest-client (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
- lefthook (1.2.2)
+ lefthook (1.2.4)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (2.0.0)
@@ -1714,7 +1714,7 @@ DEPENDENCIES
knapsack (~> 1.21.1)
kramdown (~> 2.3.1)
kubeclient (~> 4.9.3)
- lefthook (~> 1.2.2)
+ lefthook (~> 1.2.4)
letter_opener_web (~> 2.0.0)
license_finder (~> 7.0)
licensee (~> 9.15)
diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
new file mode 100644
index 00000000000..4638ba8a268
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
@@ -0,0 +1,160 @@
+<script>
+import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { glEmojiTag } from '~/emoji';
+import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '../utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '../constants';
+
+export default {
+ name: 'SecurityPatchUpgradeAlertModal',
+ i18n: {
+ modalTitle: s__('VersionCheck|Important notice - Critical security release'),
+ modalBodyNoStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately.',
+ ),
+ modalBodyStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}.',
+ ),
+ modalDetails: s__('VersionCheck|%{details}'),
+ learnMore: s__('VersionCheck|Learn more about this critical security release.'),
+ primaryButtonText: s__('VersionCheck|Upgrade now'),
+ secondaryButtonText: s__('VersionCheck|Remind me again in 3 days'),
+ },
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ currentVersion: {
+ type: String,
+ required: true,
+ },
+ details: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ latestStableVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ visible: true,
+ };
+ },
+ computed: {
+ alertEmoji() {
+ return glEmojiTag('rotating_light');
+ },
+ modalBody() {
+ if (this.latestStableVersions?.length > 0) {
+ return this.$options.i18n.modalBodyStableVersions;
+ }
+
+ return this.$options.i18n.modalBodyNoStableVersions;
+ },
+ modalDetails() {
+ return sprintf(this.$options.i18n.modalDetails, { details: this.details });
+ },
+ latestStableVersionsStrings() {
+ return this.latestStableVersions?.length > 0 ? this.latestStableVersions.join(', ') : '';
+ },
+ },
+ created() {
+ if (getHideAlertModalCookie(this.currentVersion)) {
+ this.visible = false;
+ return;
+ }
+
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ },
+ methods: {
+ dispatchTrackingEvent(action, label) {
+ this.track(action, {
+ label,
+ property: this.currentVersion,
+ });
+ },
+ trackLearnMoreClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ },
+ trackRemindMeLaterClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ setHideAlertModalCookie(this.currentVersion);
+ this.$refs.alertModal.hide();
+ },
+ trackUpgradeNowClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ setHideAlertModalCookie(this.currentVersion);
+ },
+ trackModalDismissed() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ },
+ },
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="alertModal"
+ :modal-id="$options.ALERT_MODAL_ID"
+ :visible="visible"
+ @close="trackModalDismissed"
+ >
+ <template #modal-title>
+ <span v-safe-html:[$options.safeHtmlConfig]="alertEmoji"></span>
+ <span data-testid="alert-modal-title">{{ $options.i18n.modalTitle }}</span>
+ </template>
+ <template #default>
+ <div data-testid="alert-modal-body" class="gl-mb-6">
+ <gl-sprintf :message="modalBody">
+ <template #currentVersion>
+ <span class="gl-font-weight-bold">{{ currentVersion }}</span>
+ </template>
+ <template #latestStableVersions>
+ <span class="gl-font-weight-bold">{{ latestStableVersionsStrings }}</span>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div v-if="details" data-testid="alert-modal-details" class="gl-mb-6">
+ {{ modalDetails }}
+ </div>
+ <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </template>
+ <template #modal-footer>
+ <gl-button data-testid="alert-modal-remind-button" @click="trackRemindMeLaterClick">{{
+ $options.i18n.secondaryButtonText
+ }}</gl-button>
+ <gl-button
+ data-testid="alert-modal-upgrade-button"
+ :href="$options.UPGRADE_DOCS_URL"
+ variant="confirm"
+ @click="trackUpgradeNowClick"
+ >{{ $options.i18n.primaryButtonText }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/constants.js b/app/assets/javascripts/gitlab_version_check/constants.js
index 43759c79b5b..049397148ab 100644
--- a/app/assets/javascripts/gitlab_version_check/constants.js
+++ b/app/assets/javascripts/gitlab_version_check/constants.js
@@ -9,3 +9,23 @@ export const STATUS_TYPES = {
export const UPGRADE_DOCS_URL = helpPagePath('update/index');
export const ABOUT_RELEASES_PAGE = 'https://about.gitlab.com/releases/categories/releases/';
+
+export const ALERT_MODAL_ID = 'security-patch-upgrade-alert-modal';
+
+export const COOKIE_EXPIRATION = 3;
+
+export const COOKIE_SUFFIX = '-hide-alert-modal';
+
+export const TRACKING_ACTIONS = {
+ RENDER: 'render',
+ CLICK_LINK: 'click_link',
+ CLICK_BUTTON: 'click_button',
+};
+
+export const TRACKING_LABELS = {
+ MODAL: 'security_patch_upgrade_alert_modal',
+ LEARN_MORE_LINK: 'security_patch_upgrade_alert_modal_learn_more',
+ REMIND_ME_BTN: 'security_patch_upgrade_alert_modal_remind_3_days',
+ UPGRADE_BTN_LINK: 'security_patch_upgrade_alert_modal_upgrade_now',
+ DISMISS: 'security_patch_upgrade_alert_modal_close',
+};
diff --git a/app/assets/javascripts/gitlab_version_check/index.js b/app/assets/javascripts/gitlab_version_check/index.js
index b3180c2d1ba..edb7e9abe49 100644
--- a/app/assets/javascripts/gitlab_version_check/index.js
+++ b/app/assets/javascripts/gitlab_version_check/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue';
import SecurityPatchUpgradeAlert from './components/security_patch_upgrade_alert.vue';
+import SecurityPatchUpgradeAlertModal from './components/security_patch_upgrade_alert_modal.vue';
const mountGitlabVersionCheckBadge = (el) => {
const { size, version } = el.dataset;
@@ -51,16 +52,46 @@ const mountSecurityPatchUpgradeAlert = (el) => {
}
};
+const mountSecurityPatchUpgradeAlertModal = (el) => {
+ const { currentVersion, version } = el.dataset;
+
+ try {
+ const { details, latestStableVersions } = convertObjectPropsToCamelCase(JSON.parse(version));
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SecurityPatchUpgradeAlertModal, {
+ props: {
+ currentVersion,
+ details,
+ latestStableVersions,
+ },
+ });
+ },
+ });
+ } catch {
+ return null;
+ }
+};
+
export default () => {
const renderedApps = [];
const securityPatchUpgradeAlert = document.getElementById('js-security-patch-upgrade-alert');
+ const securityPatchUpgradeAlertModal = document.getElementById(
+ 'js-security-patch-upgrade-alert-modal',
+ );
const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
if (securityPatchUpgradeAlert) {
renderedApps.push(mountSecurityPatchUpgradeAlert(securityPatchUpgradeAlert));
}
+ if (securityPatchUpgradeAlertModal) {
+ renderedApps.push(mountSecurityPatchUpgradeAlertModal(securityPatchUpgradeAlertModal));
+ }
+
renderedApps.push(...versionCheckBadges.map((el) => mountGitlabVersionCheckBadge(el)));
return renderedApps;
diff --git a/app/assets/javascripts/gitlab_version_check/utils.js b/app/assets/javascripts/gitlab_version_check/utils.js
new file mode 100644
index 00000000000..d2f4349483c
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/utils.js
@@ -0,0 +1,18 @@
+import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from './constants';
+
+const buildKey = (currentVersion) => {
+ return `${currentVersion}${COOKIE_SUFFIX}`;
+};
+
+export const setHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ setCookie(key, true, { expires: COOKIE_EXPIRATION });
+};
+
+export const getHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ return parseBoolean(getCookie(key));
+};
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index c871489fc07..376d71c7b31 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -460,19 +460,17 @@ export default {
:work-item-type="workItemType"
@error="updateError = $event"
/>
- <template v-if="workItemsMvcEnabled">
- <work-item-milestone
- v-if="workItemMilestone"
- :work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.milestone"
- :work-item-type="workItemType"
- :fetch-by-iid="fetchByIid"
- :query-variables="queryVariables"
- :can-update="canUpdate"
- :full-path="fullPath"
- @error="updateError = $event"
- />
- </template>
+ <work-item-milestone
+ v-if="workItemMilestone"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
<work-item-weight
v-if="workItemWeight"
class="gl-mb-5"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 4fcdd19784d..65855d58d67 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -122,7 +122,7 @@ export default {
confidential: this.parentConfidential,
};
- if (this.associateMilestone) {
+ if (this.parentMilestoneId) {
workItemInput = {
...workItemInput,
milestoneWidget: {
@@ -167,9 +167,6 @@ export default {
parentMilestoneId() {
return this.parentMilestone?.id;
},
- associateMilestone() {
- return this.parentMilestoneId && this.workItemsMvcEnabled;
- },
isSubmitButtonDisabled() {
return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
},
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index 42edc328fa1..b01320ce3ec 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -64,30 +64,32 @@ module ProductAnalyticsTracking
def event_enabled?(event)
events_to_ff = {
- g_analytics_valuestream: :route_hll_to_snowplow,
-
- i_search_paid: :route_hll_to_snowplow_phase2,
- i_search_total: :route_hll_to_snowplow_phase2,
- i_search_advanced: :route_hll_to_snowplow_phase2,
- i_ecosystem_jira_service_list_issues: :route_hll_to_snowplow_phase2,
- users_viewing_analytics_group_devops_adoption: :route_hll_to_snowplow_phase2,
- i_analytics_dev_ops_adoption: :route_hll_to_snowplow_phase2,
- i_analytics_dev_ops_score: :route_hll_to_snowplow_phase2,
- p_analytics_merge_request: :route_hll_to_snowplow_phase2,
- i_analytics_instance_statistics: :route_hll_to_snowplow_phase2,
- g_analytics_contribution: :route_hll_to_snowplow_phase2,
- p_analytics_pipelines: :route_hll_to_snowplow_phase2,
- p_analytics_code_reviews: :route_hll_to_snowplow_phase2,
- p_analytics_valuestream: :route_hll_to_snowplow_phase2,
- p_analytics_insights: :route_hll_to_snowplow_phase2,
- p_analytics_issues: :route_hll_to_snowplow_phase2,
- p_analytics_repo: :route_hll_to_snowplow_phase2,
- g_analytics_insights: :route_hll_to_snowplow_phase2,
- g_analytics_issues: :route_hll_to_snowplow_phase2,
- g_analytics_productivity: :route_hll_to_snowplow_phase2,
- i_analytics_cohorts: :route_hll_to_snowplow_phase2
+ g_analytics_valuestream: '',
+
+ i_search_paid: :_phase2,
+ i_search_total: :_phase2,
+ i_search_advanced: :_phase2,
+ i_ecosystem_jira_service_list_issues: :_phase2,
+ users_viewing_analytics_group_devops_adoption: :_phase2,
+ i_analytics_dev_ops_adoption: :_phase2,
+ i_analytics_dev_ops_score: :_phase2,
+ p_analytics_merge_request: :_phase2,
+ i_analytics_instance_statistics: :_phase2,
+ g_analytics_contribution: :_phase2,
+ p_analytics_pipelines: :_phase2,
+ p_analytics_code_reviews: :_phase2,
+ p_analytics_valuestream: :_phase2,
+ p_analytics_insights: :_phase2,
+ p_analytics_issues: :_phase2,
+ p_analytics_repo: :_phase2,
+ g_analytics_insights: :_phase2,
+ g_analytics_issues: :_phase2,
+ g_analytics_productivity: :_phase2,
+ i_analytics_cohorts: :_phase2,
+
+ g_compliance_dashboard: :_phase4
}
- Feature.enabled?(events_to_ff[event.to_sym], tracking_namespace_source)
+ Feature.enabled?("route_hll_to_snowplow#{events_to_ff[event.to_sym]}", tracking_namespace_source)
end
end
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 42f4f99d4a9..a3de875c196 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -55,6 +55,7 @@ module Resolvers
last_edited_by: :last_edited_by,
assignees: :assignees,
parent: :work_item_parent,
+ children: { work_item_children: [:author, { project: :project_feature }] },
labels: :labels,
milestone: :milestone
}
diff --git a/app/graphql/types/ci/freeze_period_status_enum.rb b/app/graphql/types/ci/freeze_period_status_enum.rb
new file mode 100644
index 00000000000..aebd0f537e9
--- /dev/null
+++ b/app/graphql/types/ci/freeze_period_status_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class FreezePeriodStatusEnum < BaseEnum
+ graphql_name 'CiFreezePeriodStatus'
+ description 'Deploy freeze period status'
+
+ value 'ACTIVE', value: :active, description: 'Freeze period is active.'
+ value 'INACTIVE', value: :inactive, description: 'Freeze period is inactive.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/freeze_period_type.rb b/app/graphql/types/ci/freeze_period_type.rb
new file mode 100644
index 00000000000..c1d0e9ce3b4
--- /dev/null
+++ b/app/graphql/types/ci/freeze_period_type.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class FreezePeriodType < BaseObject
+ graphql_name 'CiFreezePeriod'
+ description 'Represents a deployment freeze window of a project'
+
+ authorize :read_freeze_period
+
+ present_using ::Ci::FreezePeriodPresenter
+
+ field :status, Types::Ci::FreezePeriodStatusEnum,
+ description: 'Freeze period status.',
+ null: false
+
+ field :start_cron, GraphQL::Types::String,
+ description: 'Start of the freeze period in cron format.',
+ null: false,
+ method: :freeze_start
+
+ field :end_cron, GraphQL::Types::String,
+ description: 'End of the freeze period in cron format.',
+ null: false,
+ method: :freeze_end
+
+ field :start_time, Types::TimeType,
+ description: 'Timestamp (UTC) of when the current/next active period starts.',
+ null: true
+
+ field :end_time, Types::TimeType,
+ description: 'Timestamp (UTC) of when the current/next active period ends.',
+ null: true,
+ method: :time_end_from_now
+ end
+ end
+end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 2249fa49f06..5f58fc38540 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -73,6 +73,11 @@ module Types
description: 'Last deployment of the environment.',
resolver: Resolvers::Environments::LastDeploymentResolver
+ field :deploy_freezes,
+ [Types::Ci::FreezePeriodType],
+ null: true,
+ description: 'Deployment freeze periods of the environment.'
+
def tier
object.tier.to_sym
end
diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb
index 018e7298da5..4ec8ec84779 100644
--- a/app/graphql/types/work_items/widgets/hierarchy_type.rb
+++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb
@@ -39,7 +39,10 @@ module Types
alias_method :has_children, :has_children?
def children
- object.children.inc_relations_for_permission_check
+ relation = object.children
+ relation = relation.inc_relations_for_permission_check unless object.children.loaded?
+
+ relation
end
end
# rubocop:enable Graphql/AuthorizeTypes
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index da0bbbacddd..1bf32e04a15 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -4,6 +4,10 @@ module Ci
class FreezePeriod < Ci::ApplicationRecord
include StripAttribute
include Ci::NamespacedModelName
+ include Gitlab::Utils::StrongMemoize
+
+ STATUS_ACTIVE = :active
+ STATUS_INACTIVE = :inactive
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
@@ -14,5 +18,60 @@ module Ci
validates :freeze_start, cron: true, presence: true
validates :freeze_end, cron: true, presence: true
validates :cron_timezone, cron_freeze_period_timezone: true, presence: true
+
+ def active?
+ status == STATUS_ACTIVE
+ end
+
+ def status
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:status") do
+ within_freeze_period? ? STATUS_ACTIVE : STATUS_INACTIVE
+ end
+ end
+
+ def time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_start") do
+ freeze_start_parsed_cron.previous_time_from(time_zone_now)
+ end
+ end
+
+ def next_time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:next_time_start") do
+ freeze_start_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_now
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_now") do
+ freeze_end_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_start") do
+ freeze_end_parsed_cron.next_time_from(time_start)
+ end
+ end
+
+ private
+
+ def within_freeze_period?
+ time_start <= time_zone_now && time_zone_now <= time_end_from_start
+ end
+
+ def freeze_start_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_start, cron_timezone)
+ end
+ strong_memoize_attr :freeze_start_parsed_cron
+
+ def freeze_end_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_end, cron_timezone)
+ end
+ strong_memoize_attr :freeze_end_parsed_cron
+
+ def time_zone_now
+ Time.zone.now
+ end
+ strong_memoize_attr :time_zone_now
end
end
diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb
deleted file mode 100644
index e810bb3f229..00000000000
--- a/app/models/ci/freeze_period_status.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class FreezePeriodStatus
- attr_reader :project
-
- def initialize(project:)
- @project = project
- end
-
- def execute
- project.freeze_periods.any? { |period| within_freeze_period?(period) }
- end
-
- def within_freeze_period?(period)
- start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
- end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
-
- start_freeze = start_freeze_cron.previous_time_from(time_zone_now)
- end_freeze = end_freeze_cron.next_time_from(start_freeze)
-
- start_freeze <= time_zone_now && time_zone_now <= end_freeze
- end
-
- private
-
- def time_zone_now
- @time_zone_now ||= Time.zone.now
- end
- end
-end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 181a2bbbb98..632fd0fd24d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -725,7 +725,7 @@ module Ci
def freeze_period?
strong_memoize(:freeze_period) do
- Ci::FreezePeriodStatus.new(project: project).execute
+ project.freeze_periods.any?(&:active?)
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 684226907d4..a5e958a0645 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -507,6 +507,12 @@ class Environment < ApplicationRecord
environment_type.nil?
end
+ def deploy_freezes
+ Gitlab::SafeRequestStore.fetch("project:#{project_id}:freeze_periods_for_environments") do
+ project.freeze_periods
+ end
+ end
+
private
# We deliberately avoid using AddressableUrlValidator to allow users to update their environments even if they have
diff --git a/app/models/project.rb b/app/models/project.rb
index b250fe87fdb..65785e81f1b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,23 +89,21 @@ class Project < ApplicationRecord
cache_markdown_field :description, pipeline: :description
- default_value_for :packages_enabled, true
- default_value_for :archived, false
- default_value_for :resolve_outdated_diff_discussions, false
- default_value_for(:repository_storage) do
- Repository.pick_storage_shard
- end
+ attribute :packages_enabled, default: true
+ attribute :archived, default: false
+ attribute :resolve_outdated_diff_discussions, default: false
+ attribute :repository_storage, default: -> { Repository.pick_storage_shard }
+ attribute :shared_runners_enabled, default: -> { Gitlab::CurrentSettings.shared_runners_enabled }
+ attribute :only_allow_merge_if_all_discussions_are_resolved, default: false
+ attribute :remove_source_branch_after_merge, default: true
+ attribute :autoclose_referenced_issues, default: true
+ attribute :ci_config_path, default: -> { Gitlab::CurrentSettings.default_ci_config_path }
- default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
- default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- default_value_for :remove_source_branch_after_merge, true
- default_value_for :autoclose_referenced_issues, true
- default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required },
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index ed6f9d161a6..0810c520f7e 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -38,6 +38,18 @@ class WorkItem < Issue
end
end
+ def ancestors
+ hierarchy.ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_base_and_ancestors
+ hierarchy(same_type: true).base_and_ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_descendants_depth
+ hierarchy(same_type: true).max_descendants_depth.to_i
+ end
+
private
override :parent_link_confidentiality
@@ -56,6 +68,13 @@ class WorkItem < Issue
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
end
+
+ def hierarchy(options = {})
+ base = self.class.where(id: id)
+ base = base.where(work_item_type_id: work_item_type_id) if options[:same_type]
+
+ ::Gitlab::WorkItems::WorkItemHierarchy.new(base, options: options)
+ end
end
WorkItem.prepend_mod
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 9df2f026bd0..875e6495587 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -15,6 +15,7 @@ module WorkItems
validate :validate_child_type
validate :validate_parent_type
validate :validate_hierarchy_restrictions
+ validate :validate_cyclic_reference
validate :validate_same_project
validate :validate_max_children
validate :validate_confidentiality
@@ -98,6 +99,31 @@ module WorkItems
if restriction.nil?
errors.add :work_item, _('is not allowed to add this type of parent')
+ return
+ end
+
+ validate_depth(restriction.maximum_depth)
+ end
+
+ def validate_depth(depth)
+ return unless depth
+ return if work_item.work_item_type_id != work_item_parent.work_item_type_id
+
+ if work_item_parent.same_type_base_and_ancestors.count + work_item.same_type_descendants_depth > depth
+ errors.add :work_item, _('reached maximum depth')
+ end
+ end
+
+ def validate_cyclic_reference
+ return unless db_restrictions_enabled?
+ return unless work_item_parent&.id && work_item&.id
+
+ if work_item.id == work_item_parent.id
+ errors.add :work_item, _('is not allowed to point to itself')
+ end
+
+ if work_item_parent.ancestors.detect { |ancestor| work_item.id == ancestor.id }
+ errors.add :work_item, _('is already present in ancestors')
end
end
end
diff --git a/app/policies/ci/freeze_period_policy.rb b/app/policies/ci/freeze_period_policy.rb
index 60e53a7b2f9..9e2cca5e5a2 100644
--- a/app/policies/ci/freeze_period_policy.rb
+++ b/app/policies/ci/freeze_period_policy.rb
@@ -2,6 +2,6 @@
module Ci
class FreezePeriodPolicy < BasePolicy
- delegate { @subject.resource_parent }
+ delegate { @subject.project }
end
end
diff --git a/app/presenters/ci/freeze_period_presenter.rb b/app/presenters/ci/freeze_period_presenter.rb
new file mode 100644
index 00000000000..064197f34dd
--- /dev/null
+++ b/app/presenters/ci/freeze_period_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Ci::FreezePeriod, as: :freeze_period
+
+ def start_time
+ return freeze_period.time_start if freeze_period.active?
+
+ freeze_period.next_time_start
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index f6635ad17ef..9cfc3100078 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -1,6 +1,6 @@
.settings-content
= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting )
+ = form_errors(@application_setting)
%fieldset
.form-group
diff --git a/app/views/admin/application_settings/_default_branch.html.haml b/app/views/admin/application_settings/_default_branch.html.haml
index 7be4bac02fd..67de5ffb2b9 100644
--- a/app/views/admin/application_settings/_default_branch.html.haml
+++ b/app/views/admin/application_settings/_default_branch.html.haml
@@ -8,7 +8,7 @@
= f.label :default_branch_name, _('Initial default branch name'), class: 'label-light'
= f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input'
%span.form-text.text-muted
- = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name } ).html_safe
+ = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name }).html_safe
= render 'shared/default_branch_protection', f: f
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
index 3e7803f59e5..d6860cc08ac 100644
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ b/app/views/admin/application_settings/service_usage_data.html.haml
@@ -10,10 +10,10 @@
%h3= name
- if @service_ping_data_present
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } }) do
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
%span.js-text.gl-display-inline= _('Preview payload')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }) do
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
%span.js-text.gl-display-inline= _('Download payload')
%pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index dfd3b87c674..7699445740a 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -17,14 +17,14 @@
= f.label :broadcast_type, _('Type')
.col-sm-10
= f.select :broadcast_type, broadcast_type_options, {}, class: 'form-control js-broadcast-message-type'
- .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner? ) }
+ .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner?) }
.col-sm-2.col-form-label
= f.label :theme, _("Theme")
.col-sm-10
.input-group
= f.select :theme, broadcast_theme_options, {}, class: 'form-control js-broadcast-message-theme'
- .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner? ) }
+ .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner?) }
.col-sm-2.col-form-label.pt-0
= f.label :starts_at, _("Dismissable")
.col-sm-10
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 3121cd2ae59..a24cd000464 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -14,11 +14,11 @@
icon: 'pencil',
button_options: { title: _('Edit'),
'aria-label' => _('Edit'),
- class: button_classes } )
+ class: button_classes })
= render Pajamas::ButtonComponent.new(category: :tertiary,
href: url_for([:admin, @user, identity]),
icon: 'remove',
button_options: { title: _('Delete'),
'aria-label' => _('Delete identity'),
class: button_classes,
- data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } } )
+ data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } })
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index c7c30673d74..b6a97887b5c 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -26,7 +26,7 @@
.controls.gl-flex-shrink-0.gl-ml-5
= render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: dom_id(project, :edit) }) do
= s_('Edit')
- = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } } ) do
+ = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } }) do
= s_('AdminProjects|Delete')
= paginate @projects, theme: 'gitlab'
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index f6d05959d2e..efd2e53e100 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil})
%section
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index 1146063969b..9ea83397348 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('Runners|Runners')
-#js-group-runners{ data: group_runners_data_attributes(@group).merge( { group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token } ) }
+#js-group-runners{ data: group_runners_data_attributes(@group).merge({ group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token }) }
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 351d603ece0..079123e989e 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -21,7 +21,7 @@
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { data: { qa_selector: 'import_project_button' }}) do
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { qa_selector: 'import_project_button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 8815dec5a6b..717175e8eb3 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -14,7 +14,7 @@
%span.nav-item-name
= _('Overview')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" }) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
= _('Overview')
@@ -82,7 +82,7 @@
= _('Monitoring')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
- = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" }) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
= _('Monitoring')
@@ -117,7 +117,7 @@
%span.nav-item-name
= _('Messages')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" }) do
= link_to admin_broadcast_messages_path do
%strong.fly-out-top-item-name
= _('Messages')
@@ -129,7 +129,7 @@
%span.nav-item-name
= _('System Hooks')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" }) do
= link_to admin_hooks_path do
%strong.fly-out-top-item-name
= _('System Hooks')
@@ -141,7 +141,7 @@
%span.nav-item-name
= _('Applications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" }) do
= link_to admin_applications_path do
%strong.fly-out-top-item-name
= _('Applications')
@@ -154,7 +154,7 @@
= _('Abuse Reports')
= gl_badge_tag number_with_delimiter(AbuseReport.count(:all)), variant: :info, size: :sm
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" }) do
= link_to admin_abuse_reports_path do
%strong.fly-out-top-item-name
= _('Abuse Reports')
@@ -170,7 +170,7 @@
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" }) do
= link_to admin_clusters_path do
%strong.fly-out-top-item-name
= _('Kubernetes')
@@ -183,7 +183,7 @@
%span.nav-item-name
= _('Spam Logs')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" }) do
= link_to admin_spam_logs_path do
%strong.fly-out-top-item-name
= _('Spam Logs')
@@ -199,7 +199,7 @@
%span.nav-item-name
= _('Deploy Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" }) do
= link_to admin_deploy_keys_path do
%strong.fly-out-top-item-name
= _('Deploy Keys')
@@ -213,7 +213,7 @@
%span.nav-item-name
= _('Labels')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" }) do
= link_to admin_labels_path do
%strong.fly-out-top-item-name
= _('Labels')
@@ -227,7 +227,7 @@
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
-# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
- = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" }) do
= link_to general_admin_application_settings_path do
%strong.fly-out-top-item-name
= _('Settings')
@@ -273,7 +273,7 @@
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
%span
= _('Network')
- = nav_link(controller: :appearances ) do
+ = nav_link(controller: :appearances) do
= link_to admin_application_settings_appearances_path do
%span
= _('Appearance')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 0e3327935ca..e1978009114 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -12,7 +12,7 @@
%span.nav-item-name
= _('Profile')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" }) do
= link_to profile_path do
%strong.fly-out-top-item-name
= _('Profile')
@@ -23,7 +23,7 @@
%span.nav-item-name
= _('Account')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" }) do
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
@@ -36,7 +36,7 @@
%span.nav-item-name
= _('Applications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" }) do
= link_to applications_profile_path do
%strong.fly-out-top-item-name
= _('Applications')
@@ -47,7 +47,7 @@
%span.nav-item-name
= _('Chat')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" }) do
= link_to profile_chat_names_path do
%strong.fly-out-top-item-name
= _('Chat')
@@ -59,7 +59,7 @@
%span.nav-item-name
= _('Access Tokens')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" }) do
= link_to profile_personal_access_tokens_path do
%strong.fly-out-top-item-name
= _('Access Tokens')
@@ -70,7 +70,7 @@
%span.nav-item-name
= _('Emails')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" }) do
= link_to profile_emails_path do
%strong.fly-out-top-item-name
= _('Emails')
@@ -82,7 +82,7 @@
%span.nav-item-name
= _('Password')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" }) do
= link_to edit_profile_password_path do
%strong.fly-out-top-item-name
= _('Password')
@@ -93,7 +93,7 @@
%span.nav-item-name
= _('Notifications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" }) do
= link_to profile_notifications_path do
%strong.fly-out-top-item-name
= _('Notifications')
@@ -104,7 +104,7 @@
%span.nav-item-name
= _('SSH Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_keys_path do
%strong.fly-out-top-item-name
= _('SSH Keys')
@@ -115,7 +115,7 @@
%span.nav-item-name
= _('GPG Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_gpg_keys_path do
%strong.fly-out-top-item-name
= _('GPG Keys')
@@ -126,7 +126,7 @@
%span.nav-item-name
= _('Preferences')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" }) do
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
= _('Preferences')
@@ -137,7 +137,7 @@
%span.nav-item-name
= _('Active Sessions')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" }) do
= link_to profile_active_sessions_path do
%strong.fly-out-top-item-name
= _('Active Sessions')
@@ -148,7 +148,7 @@
%span.nav-item-name
= _('Authentication log')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" }) do
= link_to audit_log_profile_path do
%strong.fly-out-top-item-name
= _('Authentication Log')
diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml
index bdf2a1136d3..d6812821966 100644
--- a/app/views/notify/autodevops_disabled_email.html.haml
+++ b/app/views/notify/autodevops_disabled_email.html.haml
@@ -11,7 +11,7 @@
- link_style = "color: #1b69b6; text-decoration:none;"
- pipeline_link = link_to("\##{@pipeline.iid}", pipeline_url(@pipeline), style: link_style).html_safe
- project_link = link_to(@project.name, project_url(@project), style: link_style).html_safe
- - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style ).html_safe
+ - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style).html_safe
- settings_link = link_to(s_('Notify|CI/CD project settings'), project_settings_ci_cd_url(@project), style: link_style).html_safe
= s_('Notify|The Auto DevOps pipeline failed for pipeline %{pipeline_link} and has been disabled for %{project_link}. In order to use the Auto DevOps pipeline with your project, please review the %{supported_langs_link}, adjust your project accordingly, and turn on the Auto DevOps pipeline within your %{settings_link}.').html_safe % { pipeline_link: pipeline_link, project_link: project_link, supported_langs_link: supported_langs_link, settings_link: settings_link }
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
index c77a863d1a4..666aa45540e 100644
--- a/app/views/notify/issue_moved_email.html.haml
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -2,6 +2,6 @@
= s_('Notify|Issue was moved to another project.')
- if @can_access_project
%p
- = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) } ).html_safe
+ = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) }).html_safe
- else
= s_("Notify|You don't have access to the project.")
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index be9140b23ff..8ef2819fb9b 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -103,7 +103,7 @@
- supported_characters = %w(" ' ` &#40; [ { < * _).map { |char| "<code>#{char}</code>" }.join(', ')
= f.gitlab_ui_checkbox_component :markdown_surround_selection,
s_('Preferences|Surround text selection when typing quotes or brackets'),
- help_text: sprintf(s_( "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe
+ help_text: sprintf(s_("Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe
.form-group
= f.gitlab_ui_checkbox_component :markdown_automatic_lists,
s_('Preferences|Automatically add new list items'),
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 249c474587c..4fe68c1ce1a 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -4,12 +4,12 @@
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
= dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } })
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } })
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } })
.metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } })
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } })
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } })
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 11984a9d6f6..8ff6d348d95 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -34,7 +34,7 @@
- if load_diff_files_async
- url = url_for(safe_params.merge(action: 'diff_files'))
.js-diffs-batch{ data: { diff_files_path: url } }
- = gl_loading_icon( size: "md", css_class: "gl-mt-4" )
+ = gl_loading_icon(size: "md", css_class: "gl-mt-4")
- else
= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index 93cb5ddd7e2..3cc419716e5 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -1,7 +1,7 @@
- @can_bulk_update = false
- page_title _("Service Desk")
-- add_page_specific_style 'page_bundles/issues_list'
+- add_page_specific_style 'page_bundles/issuable_list'
- content_for :breadcrumbs_extra do
= render "projects/issues/service_desk/nav_btns", show_export_button: false, show_rss_button: false
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 06123aea960..70bb97b7625 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title _("Graph")
- page_title _("Graph"), @ref
-- network_path = Feature.enabled?(:use_ref_type_parameter) ? project_network_path(@project, @id, ref_type: @ref_type ) : project_network_path(@project, @id)
+- network_path = Feature.enabled?(:use_ref_type_parameter) ? project_network_path(@project, @id, ref_type: @ref_type) : project_network_path(@project, @id)
= render "head"
.gl-mt-5
.project-network.gl-border-1.gl-border-solid.gl-border-gray-300
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 16312da1353..32e67fdadb8 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -12,7 +12,7 @@
- if verification_enabled
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
.domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip }
- = sprite_icon("status_#{status}" )
+ = sprite_icon("status_#{status}")
.domain-name
= external_link(domain.url, domain.url)
- if domain.certificate
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 9dc12a2ca41..0de31f59033 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -6,7 +6,7 @@
%td.branch-name-cell.gl-text-truncate{ role: 'cell', data: { label: s_("PipelineSchedules|Target") } }
%div
- if pipeline_schedule.for_tag?
- = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!' )
+ = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!')
- else
= sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!')
- if pipeline_schedule.ref.present?
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 59b7b6ca334..910aab6da72 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil})
%section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index ed06c90efa8..f48404c7407 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,7 +2,7 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true ) do |c|
+ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true) do |c|
= c.body do
= @error
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 1645c2695b5..8a626f1620b 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -32,17 +32,17 @@
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
%li
= render Pajamas::ButtonComponent.new(category: :tertiary,
- button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } } ) do
+ button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
= _('Promote to group label')
%li
%span
= render Pajamas::ButtonComponent.new(category: :tertiary,
- button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } } ) do
+ button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
= _('Delete')
- if current_user
%li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3
- if label.can_subscribe_to_label_in_different_levels?
- = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
= render Pajamas::ButtonComponent.new(button_options: { class: 'gl-w-full', data: { toggle: 'dropdown' } }) do
@@ -51,11 +51,11 @@
.dropdown-menu.dropdown-open-left
%ul
%li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } ) do
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }) do
= _('Subscribe at project level')
%li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } ) do
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }) do
= _('Subscribe at group level')
- else
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
= label_subscription_toggle_button_text(label, @project)
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index ef41dc9bb79..0053f2fe444 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,6 +1,6 @@
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
-= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } } ) do
+= gl_tabs_nav({class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } }) do
= gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do
= _('Open')
= gl_tab_counter_badge counts[:opened], { class: count_badge_classes }
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 8e4b8d6d428..8f2b9fc06e3 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,6 +1,6 @@
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
-= gl_tabs_nav( {class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } } ) do
+= gl_tabs_nav({class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } }) do
= gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
= _('All')
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes })
diff --git a/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml
index 7604445abb2..9fe1400e877 100644
--- a/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml
+++ b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml
@@ -1,3 +1,4 @@
- return unless show_security_patch_upgrade_alert?
#js-security-patch-upgrade-alert{ data: { "current_version": Gitlab.version_info } }
+#js-security-patch-upgrade-alert-modal{ data: { "current_version": Gitlab.version_info, "version": gitlab_version_check.to_json } }
diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml
index 8ee0ddfa1b1..c74dbfd8b15 100644
--- a/app/views/shared/integrations/prometheus/_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_metrics.html.haml
@@ -25,8 +25,8 @@
.card.hidden.js-panel-missing-env-vars
.card-header
- = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' )
- = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' )
+ = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right')
+ = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden')
= s_('PrometheusService|Missing environment variable')
= gl_badge_tag 0, nil, class: 'js-env-var-count'
.card-body.hidden
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 327461b25fd..39a123f4775 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -101,7 +101,7 @@
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= sprite_icon('long-arrow')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
- = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } } ) do
+ = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } }) do
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 51f49c7ca8e..0f6ef33d532 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -4,8 +4,8 @@
- no_issuable_templates = issuable_templates(ref_project, issuable.to_ability_name).empty?
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>'
-- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe } ).html_safe
-- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe } ).html_safe
+- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe }).html_safe
+- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe }).html_safe
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
diff --git a/app/views/shared/nav/_sidebar_submenu.html.haml b/app/views/shared/nav/_sidebar_submenu.html.haml
index 344dafe7c0f..33b48470020 100644
--- a/app/views/shared/nav/_sidebar_submenu.html.haml
+++ b/app/views/shared/nav/_sidebar_submenu.html.haml
@@ -1,5 +1,5 @@
%ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
- = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
+ = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' }) do
%span.fly-out-top-item-container
%strong.fly-out-top-item-name
= sidebar_menu.title
diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
index d10196a83cc..0244f9e2158 100644
--- a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
@@ -9,7 +9,7 @@
%td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
- = dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
+ = dropdown_tag((merge_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
- if user_merge_access_levels.any?
@@ -22,7 +22,7 @@
%td.push_access_levels-container
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
- = dropdown_tag( (push_access_levels.first&.humanize || 'Select') ,
+ = dropdown_tag((push_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
- if user_push_access_levels.any?
diff --git a/config/feature_flags/development/projects_preloader_fix.yml b/config/feature_flags/development/projects_preloader_fix.yml
index 1ad578f11a4..58c56257028 100644
--- a/config/feature_flags/development/projects_preloader_fix.yml
+++ b/config/feature_flags/development/projects_preloader_fix.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378858
milestone: '15.6'
type: development
group: group::workspace
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/route_hll_to_snowplow_phase4.yml b/config/feature_flags/development/route_hll_to_snowplow_phase4.yml
new file mode 100644
index 00000000000..a4109b15533
--- /dev/null
+++ b/config/feature_flags/development/route_hll_to_snowplow_phase4.yml
@@ -0,0 +1,8 @@
+---
+name: route_hll_to_snowplow_phase4
+introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103528"
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366767
+milestone: '15.7'
+type: development
+group: group::product intelligence
+default_enabled: false
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index daf2b635855..688806e9b22 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -15,20 +15,25 @@ Get a list of deployments in a project.
GET /projects/:id/deployments
```
-| Attribute | Type | Required | Description |
-|------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
-| `order_by` | string | no | Return deployments ordered by either one of `id`, `iid`, `created_at`, `updated_at` or `ref` fields. Default is `id`. |
-| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc`. |
-| `updated_after` | datetime | no | Return deployments updated after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
-| `updated_before` | datetime | no | Return deployments updated before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
-| `environment` | string | no | The [name of the environment](../ci/environments/index.md) to filter deployments by. |
-| `status` | string | no | The status to filter deployments by. One of `created`, `running`, `success`, `failed`, `canceled`, or `blocked`.
+| Attribute | Type | Required | Description |
+|-------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
+| `order_by` | string | no | Return deployments ordered by either one of `id`, `iid`, `created_at`, `updated_at`, `finished_at` or `ref` fields. Default is `id`. |
+| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc`. |
+| `updated_after` | datetime | no | Return deployments updated after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
+| `updated_before` | datetime | no | Return deployments updated before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
+| `finished_after` | datetime | no | Return deployments finished after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
+| `finished_before` | datetime | no | Return deployments finished before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
+| `environment` | string | no | The [name of the environment](../ci/environments/index.md) to filter deployments by. |
+| `status` | string | no | The status to filter deployments by. One of `created`, `running`, `success`, `failed`, `canceled`, or `blocked`.
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
```
+NOTE:
+When using `finished_before` or `finished_after`, you should specify the `order_by` to be `finished_at` and `status` should be `success`.
+
Example response:
```json
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c9ffbc4943d..bd0bd2e6288 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -10895,6 +10895,20 @@ CI/CD config variables.
| <a id="ciconfigvariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
| <a id="ciconfigvariablevalueoptions"></a>`valueOptions` | [`[String!]`](#string) | Value options for the variable. |
+### `CiFreezePeriod`
+
+Represents a deployment freeze window of a project.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cifreezeperiodendcron"></a>`endCron` | [`String!`](#string) | End of the freeze period in cron format. |
+| <a id="cifreezeperiodendtime"></a>`endTime` | [`Time`](#time) | Timestamp (UTC) of when the current/next active period ends. |
+| <a id="cifreezeperiodstartcron"></a>`startCron` | [`String!`](#string) | Start of the freeze period in cron format. |
+| <a id="cifreezeperiodstarttime"></a>`startTime` | [`Time`](#time) | Timestamp (UTC) of when the current/next active period starts. |
+| <a id="cifreezeperiodstatus"></a>`status` | [`CiFreezePeriodStatus!`](#cifreezeperiodstatus) | Freeze period status. |
+
### `CiGroup`
#### Fields
@@ -12422,6 +12436,7 @@ Describes where code is deployed for a project.
| <a id="environmentautodeleteat"></a>`autoDeleteAt` | [`Time`](#time) | When the environment is going to be deleted automatically. |
| <a id="environmentautostopat"></a>`autoStopAt` | [`Time`](#time) | When the environment is going to be stopped automatically. |
| <a id="environmentcreatedat"></a>`createdAt` | [`Time`](#time) | When the environment was created. |
+| <a id="environmentdeployfreezes"></a>`deployFreezes` | [`[CiFreezePeriod!]`](#cifreezeperiod) | Deployment freeze periods of the environment. |
| <a id="environmentenvironmenttype"></a>`environmentType` | [`String`](#string) | Folder name of the environment. |
| <a id="environmentexternalurl"></a>`externalUrl` | [`String`](#string) | External URL of the environment. |
| <a id="environmentid"></a>`id` | [`ID!`](#id) | ID of the environment. |
@@ -21113,6 +21128,15 @@ Values for YAML processor result.
| <a id="ciconfigstatusinvalid"></a>`INVALID` | Configuration file is not valid. |
| <a id="ciconfigstatusvalid"></a>`VALID` | Configuration file is valid. |
+### `CiFreezePeriodStatus`
+
+Deploy freeze period status.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="cifreezeperiodstatusactive"></a>`ACTIVE` | Freeze period is active. |
+| <a id="cifreezeperiodstatusinactive"></a>`INACTIVE` | Freeze period is inactive. |
+
### `CiJobKind`
| Value | Description |
diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md
index 37054c61a85..101fdbf4c2d 100644
--- a/doc/architecture/blueprints/work_items/index.md
+++ b/doc/architecture/blueprints/work_items/index.md
@@ -61,7 +61,7 @@ All Work Item types share the same pool of predefined widgets and are customized
| description | |
| hierarchy | |
| [iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) | |
-| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | work_items_mvc |
+| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | |
| labels | |
| start and due date | |
| status\* | |
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 90f93e1db3f..3e55b334992 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -767,6 +767,28 @@ in your merge request fails.
### Text for links
+Follow these guidelines for link text.
+
+#### Standard text
+
+As much as possible, use text that follows one of these patterns:
+
+- `For more information, see [LINK TEXT](LINK)`.
+- `To [DO THIS THING], see [LINK TEXT](LINK)`
+
+For example:
+
+- `For more information, see [merge requests](../../../user/project/merge_requests/index.md).`
+- `To create a review app, see [review apps](../../../ci/review_apps/index.md).`
+
+You can expand on this text by using phrases like
+`For more information about this feature, see...`
+
+Do not to use alternate phrases, like `Learn more about...` or
+`To read more...`.
+
+#### Descriptive text rather than `here`
+
Use descriptive text for links, rather than words like `here` or `this page.`
For example, instead of:
@@ -778,6 +800,14 @@ Use:
- `For more information, see [merge requests](LINK)`.
+#### Links to issues
+
+When linking to an issue, include the issue number in the link. For example:
+
+- `For more information, see [issue 12345](LINK).`
+
+Do not use the pound sign (`issue #12345`).
+
### Links to external documentation
When possible, avoid links to external documentation. These links can easily become outdated, and are difficult to maintain.
diff --git a/doc/development/documentation/versions.md b/doc/development/documentation/versions.md
index 14a58452598..e7227a71120 100644
--- a/doc/development/documentation/versions.md
+++ b/doc/development/documentation/versions.md
@@ -203,8 +203,8 @@ We cannot guarantee future feature work, and promises
like these can raise legal issues. Instead, say that an issue exists.
For example:
-- Support for improvements is proposed in issue `[issue-number](LINK-TO-ISSUE)`.
-- You cannot do this thing, but issue `[issue-number](LINK-TO-ISSUE)` proposes to change this behavior.
+- Support for improvements is proposed in `[issue [LINK](LINK-TO-ISSUE)`.
+- You cannot do this thing, but `[issue [LINK](LINK-TO-ISSUE)` proposes to change this behavior.
You can say that we plan to remove a feature.
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 9226f8d15c9..def485b2c04 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -209,10 +209,7 @@ To set a start date:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) to feature flag named `work_items_mvc` in GitLab 15.7. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`.
-On GitLab.com, this feature is not available. The feature is not ready for production use.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.7.
You can add a task to a [milestone](project/milestones/index.md).
You can see the milestone title when you view a task.
diff --git a/lib/atlassian/jira_connect/jwt/asymmetric.rb b/lib/atlassian/jira_connect/jwt/asymmetric.rb
index 573a8022752..7c1cf1cabb6 100644
--- a/lib/atlassian/jira_connect/jwt/asymmetric.rb
+++ b/lib/atlassian/jira_connect/jwt/asymmetric.rb
@@ -77,11 +77,7 @@ module Atlassian
end
def public_key_cdn_url
- if public_key_cdn_url_setting.blank? || Feature.disabled?(:jira_connect_oauth_self_managed)
- return DEFAULT_PUBLIC_KEY_CDN_URL
- end
-
- public_key_cdn_url_setting
+ public_key_cdn_url_setting.presence || DEFAULT_PUBLIC_KEY_CDN_URL
end
def public_key_cdn_url_setting
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 55b10c008fb..9b019f16ddd 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -157,7 +157,7 @@ module Backup
end
def backup_files_realpath
- @backup_files_realpath ||= File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) )
+ @backup_files_realpath ||= File.join(Gitlab.config.backup.path, File.basename(@app_files_dir))
end
end
end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 7de6be45349..49b8ab760f3 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -98,7 +98,7 @@ module Gitlab
create_labels
- issue_type_id = WorkItems::Type.default_issue_type.id
+ issue_type_id = ::WorkItems::Type.default_issue_type.id
client.issues(repo).each do |issue|
import_issue(issue, issue_type_id)
diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb
index dd9b9cc6d55..4062132b970 100644
--- a/lib/gitlab/ci/reports/security/finding.rb
+++ b/lib/gitlab/ci/reports/security/finding.rb
@@ -98,7 +98,7 @@ module Gitlab
end
def unsafe?(severity_levels, report_types)
- severity.to_s.in?(severity_levels) && (report_types.blank? || report_type.to_s.in?(report_types) )
+ severity.to_s.in?(severity_levels) && (report_types.blank? || report_type.to_s.in?(report_types))
end
def eql?(other)
diff --git a/lib/gitlab/ci/runner_instructions.rb b/lib/gitlab/ci/runner_instructions.rb
index 01df41a06ce..bcda2fec5ba 100644
--- a/lib/gitlab/ci/runner_instructions.rb
+++ b/lib/gitlab/ci/runner_instructions.rb
@@ -91,7 +91,7 @@ module Gitlab
end
def environment
- @environment ||= OS[@os.to_sym] || ( raise Gitlab::Ci::RunnerInstructions::ArgumentError, _('Invalid OS') )
+ @environment ||= OS[@os.to_sym] || (raise Gitlab::Ci::RunnerInstructions::ArgumentError, _('Invalid OS'))
end
def validate_params
diff --git a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
index 4e38eea963b..fd99062974c 100644
--- a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
+++ b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb
@@ -19,7 +19,7 @@ module Gitlab
attr_reader :table, :value
- def initialize(table, value, partition_name: nil )
+ def initialize(table, value, partition_name: nil)
@table = table
@value = value
@partition_name = partition_name
diff --git a/lib/gitlab/database/postgres_hll/buckets.rb b/lib/gitlab/database/postgres_hll/buckets.rb
index cbc9544d905..3f64eee030e 100644
--- a/lib/gitlab/database/postgres_hll/buckets.rb
+++ b/lib/gitlab/database/postgres_hll/buckets.rb
@@ -61,7 +61,7 @@ module Gitlab
num_uniques = (
((TOTAL_BUCKETS**2) * (0.7213 / (1 + 1.079 / TOTAL_BUCKETS))) /
- (num_zero_buckets + buckets.values.sum { |bucket_hash| 2**(-1 * bucket_hash) } )
+ (num_zero_buckets + buckets.values.sum { |bucket_hash| 2**(-1 * bucket_hash) })
).to_i
if num_zero_buckets > 0 && num_uniques < 2.5 * TOTAL_BUCKETS
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 924c28e3db5..b29c75ed467 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -73,7 +73,7 @@ module Gitlab
private
def filename?(line)
- line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
+ line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
'+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
'--- /tmp/diffy', '+++ /tmp/diffy')
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 6bcf4802fbe..de66ca7305f 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -571,7 +571,7 @@ module Gitlab
end
def encode_repeated(array)
- Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) } )
+ Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) })
end
def call_find_commit(revision)
diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb
index aa66fe8a5ae..564008e7a73 100644
--- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb
+++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb
@@ -35,7 +35,7 @@ module Gitlab
Timeout.timeout(TIMEOUT_LIMIT) do
stderr_r, stderr_w = IO.pipe
- stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w )
+ stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w)
# When validation is performed on a small archive (e.g. 100 bytes)
# `wait_thr` finishes before we can get process group id. Do not
diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb
index 5057317ae01..7b031c26b72 100644
--- a/lib/gitlab/jira_import/issues_importer.rb
+++ b/lib/gitlab/jira_import/issues_importer.rb
@@ -16,7 +16,7 @@ module Gitlab
@start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id)
@imported_items_cache_key = JiraImport.already_imported_cache_key(:issues, project.id)
@job_waiter = JobWaiter.new
- @issue_type_id = WorkItems::Type.default_issue_type.id
+ @issue_type_id = ::WorkItems::Type.default_issue_type.id
end
def execute
diff --git a/lib/gitlab/memory/watchdog/monitor_state.rb b/lib/gitlab/memory/watchdog/monitor_state.rb
index 2562599d2ab..bb083fedf2c 100644
--- a/lib/gitlab/memory/watchdog/monitor_state.rb
+++ b/lib/gitlab/memory/watchdog/monitor_state.rb
@@ -7,7 +7,7 @@ module Gitlab
class Result
attr_reader :payload, :monitor_name
- def initialize(strikes_exceeded:, threshold_violated:, monitor_name:, payload: )
+ def initialize(strikes_exceeded:, threshold_violated:, monitor_name:, payload:)
@strikes_exceeded = strikes_exceeded
@threshold_violated = threshold_violated
@monitor_name = monitor_name.to_s.to_sym
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index dda28ffdf90..6a5613ddd98 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -150,7 +150,7 @@ module Gitlab
end
def get(path, args)
- Gitlab::HTTP.get(path, { query: args }.merge(http_options) )
+ Gitlab::HTTP.get(path, { query: args }.merge(http_options))
rescue *Gitlab::HTTP::HTTP_ERRORS => e
raise PrometheusClient::ConnectionError, e.message
end
diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb
index d5227e7a007..c535e103420 100644
--- a/lib/gitlab/sidekiq_daemon/memory_killer.rb
+++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb
@@ -78,7 +78,7 @@ module Gitlab
rescue StandardError => e
log_exception(e, __method__)
rescue Exception => e # rubocop:disable Lint/RescueException
- log_exception(e, __method__ )
+ log_exception(e, __method__)
raise e
end
end
@@ -188,7 +188,7 @@ module Gitlab
def increment_worker_counters(running_jobs, deadline_exceeded)
running_jobs.each do |job|
- @metrics[:sidekiq_memory_killer_running_jobs].increment( { worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded } )
+ @metrics[:sidekiq_memory_killer_running_jobs].increment({ worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded })
end
end
diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb
index df2a0658b36..a912fdbaeca 100644
--- a/lib/gitlab/tracking/incident_management.rb
+++ b/lib/gitlab/tracking/incident_management.rb
@@ -17,7 +17,7 @@ module Gitlab
details = label ? { label: label, property: v } : {}
- ::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details )
+ ::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details)
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 7360585df43..8b016a09889 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -11,7 +11,7 @@ module Gitlab
included do
scope :public_only, -> { where(visibility_level: PUBLIC) }
- scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
+ scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL]) }
scope :private_only, -> { where(visibility_level: PRIVATE) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
diff --git a/lib/gitlab/work_items/work_item_hierarchy.rb b/lib/gitlab/work_items/work_item_hierarchy.rb
new file mode 100644
index 00000000000..e71bf2bce6a
--- /dev/null
+++ b/lib/gitlab/work_items/work_item_hierarchy.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WorkItems
+ class WorkItemHierarchy < ObjectHierarchy
+ extend ::Gitlab::Utils::Override
+
+ private
+
+ def middle_table
+ ::WorkItems::ParentLink.arel_table
+ end
+
+ def from_tables(cte)
+ [objects_table, cte.table, middle_table]
+ end
+
+ override :parent_id_column
+ def parent_id_column(cte)
+ middle_table[:work_item_parent_id]
+ end
+
+ override :ancestor_conditions
+ def ancestor_conditions(cte)
+ conditions = middle_table[:work_item_parent_id].eq(objects_table[:id]).and(
+ middle_table[:work_item_id].eq(cte.table[:id])
+ )
+
+ with_type_filter(conditions, cte)
+ end
+
+ override :descendant_conditions
+ def descendant_conditions(cte)
+ conditions = middle_table[:work_item_id].eq(objects_table[:id]).and(
+ middle_table[:work_item_parent_id].eq(cte.table[:id])
+ )
+
+ with_type_filter(conditions, cte)
+ end
+
+ def with_type_filter(conditions, cte)
+ return conditions unless options[:same_type]
+
+ conditions.and(objects_table[:work_item_type_id].eq(cte.table[:work_item_type_id]))
+ end
+ end
+ end
+end
diff --git a/lib/security/ci_configuration/sast_build_action.rb b/lib/security/ci_configuration/sast_build_action.rb
index 448d4fbeacb..2b1964f7c87 100644
--- a/lib/security/ci_configuration/sast_build_action.rb
+++ b/lib/security/ci_configuration/sast_build_action.rb
@@ -68,7 +68,7 @@ module Security
end
def auto_devops_stages
- auto_devops_template = YAML.safe_load( Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content )
+ auto_devops_template = YAML.safe_load(Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content)
auto_devops_template['stages']
end
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index 1b9ff33415f..34ef4b139c3 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -10,21 +10,21 @@ namespace :gitlab do
desc 'GitLab | Sidekiq | Migrate jobs in the scheduled set to new queue names'
task schedule: :environment do
::Gitlab::SidekiqMigrateJobs
- .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) )
+ .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout))
.migrate_set('schedule')
end
desc 'GitLab | Sidekiq | Migrate jobs in the retry set to new queue names'
task retry: :environment do
::Gitlab::SidekiqMigrateJobs
- .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) )
+ .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout))
.migrate_set('retry')
end
desc 'GitLab | Sidekiq | Migrate jobs in queues outside of routing rules'
task queued: :environment do
::Gitlab::SidekiqMigrateJobs
- .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) )
+ .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout))
.migrate_queues
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b97935d849e..258d61624f2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -45306,12 +45306,21 @@ msgstr ""
msgid "Version %{versionNumber} (latest)"
msgstr ""
+msgid "VersionCheck|%{details}"
+msgstr ""
+
msgid "VersionCheck|Critical security upgrade available"
msgstr ""
+msgid "VersionCheck|Important notice - Critical security release"
+msgstr ""
+
msgid "VersionCheck|Learn more about this critical security release."
msgstr ""
+msgid "VersionCheck|Remind me again in 3 days"
+msgstr ""
+
msgid "VersionCheck|Up to date"
msgstr ""
@@ -45324,6 +45333,12 @@ msgstr ""
msgid "VersionCheck|Upgrade now"
msgstr ""
+msgid "VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately."
+msgstr ""
+
+msgid "VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}."
+msgstr ""
+
msgid "VersionCheck|You are currently on version %{currentVersion}. We strongly recommend upgrading your GitLab installation. %{link}"
msgstr ""
@@ -49082,6 +49097,9 @@ msgstr ""
msgid "is already linked to this vulnerability"
msgstr ""
+msgid "is already present in ancestors"
+msgstr ""
+
msgid "is an invalid IP address range"
msgstr ""
@@ -49124,6 +49142,9 @@ msgstr ""
msgid "is not allowed to add this type of parent"
msgstr ""
+msgid "is not allowed to point to itself"
+msgstr ""
+
msgid "is not allowed. Please use your regular email address."
msgstr ""
@@ -49836,6 +49857,9 @@ msgstr ""
msgid "reCAPTCHA site key"
msgstr ""
+msgid "reached maximum depth"
+msgstr ""
+
msgid "recent activity"
msgstr ""
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
new file mode 100644
index 00000000000..f1ed32a5f79
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
@@ -0,0 +1,202 @@
+import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { sprintf } from '~/locale';
+import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
+import * as utils from '~/gitlab_version_check/utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '~/gitlab_version_check/constants';
+
+describe('SecurityPatchUpgradeAlertModal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ currentVersion: '11.1.1',
+ };
+
+ const createComponent = (props = {}) => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMountExtended(SecurityPatchUpgradeAlertModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const expectDispatchedTracking = (action, label) => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
+ label,
+ property: defaultProps.currentVersion,
+ });
+ };
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+ const findGlModalTitle = () => wrapper.findByTestId('alert-modal-title');
+ const findGlModalBody = () => wrapper.findByTestId('alert-modal-body');
+ const findGlModalDetails = () => wrapper.findByTestId('alert-modal-details');
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findGlRemindButton = () => wrapper.findByTestId('alert-modal-remind-button');
+ const findGlUpgradeButton = () => wrapper.findByTestId('alert-modal-upgrade-button');
+
+ describe('template defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders visible critical security alert modal', () => {
+ expect(findGlModal().props('visible')).toBe(true);
+ });
+
+ it('renders the modal title correctly', () => {
+ expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
+ });
+
+ it('renders modal body without suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ }),
+ );
+ });
+
+ it('does not render modal details', () => {
+ expect(findGlModalDetails().exists()).toBe(false);
+ });
+
+ it(`tracks render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expectDispatchedTracking(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.DISMISS} when close button clicked`, async () => {
+ await findGlModal().vm.$emit('close');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ });
+
+ describe('Learn more link', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
+ expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.LEARN_MORE_LINK} when clicked`, async () => {
+ await findGlLink().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ });
+ });
+
+ describe('Remind me button', () => {
+ beforeEach(() => {
+ wrapper.vm.$refs.alertModal.hide = jest.fn();
+ });
+
+ it('renders with correct text', () => {
+ expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+
+ it('hides the modal', async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('Upgrade button', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
+ expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.UPGRADE_BTN_LINK} when clicked`, async () => {
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+ });
+ });
+
+ describe('template with latestStableVersions', () => {
+ const latestStableVersions = ['88.8.3', '89.9.9', '90.0.0'];
+
+ beforeEach(() => {
+ createComponent({ latestStableVersions });
+ });
+
+ it('renders modal body with suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ latestStableVersions: latestStableVersions.join(', '),
+ }),
+ );
+ });
+ });
+
+ describe('template with details', () => {
+ const details = 'This is some details about the upgrade';
+
+ beforeEach(() => {
+ createComponent({ details });
+ });
+
+ it('renders modal details', () => {
+ expect(findGlModalDetails().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
+ );
+ });
+ });
+
+ describe('when modal is hidden by cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getHideAlertModalCookie').mockReturnValue(true);
+ createComponent();
+ });
+
+ it('renders modal with visibility false', () => {
+ expect(findGlModal().props('visible')).toBe(false);
+ });
+
+ it(`does not track render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, TRACKING_ACTIONS.RENDER, {
+ label: TRACKING_LABELS.MODAL,
+ property: defaultProps.currentVersion,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js
index 9e4d50f2cfb..92bc103cede 100644
--- a/spec/frontend/gitlab_version_check/index_spec.js
+++ b/spec/frontend/gitlab_version_check/index_spec.js
@@ -9,15 +9,19 @@ import {
VERSION_BADGE_TEXT,
SECURITY_PATCH_FIXTURE,
SECURITY_PATCH_FINDER,
- SECURITY_BATCH_TEXT,
+ SECURITY_PATCH_TEXT,
+ SECURITY_MODAL_FIXTURE,
+ SECURITY_MODAL_FINDER,
+ SECURITY_MODAL_TEXT,
} from './mock_data';
describe('initGitlabVersionCheck', () => {
- let vueApps;
+ let wrapper;
const createApp = (fixture) => {
setHTMLFixture(fixture);
- vueApps = initGitlabVersionCheck();
+ initGitlabVersionCheck();
+ wrapper = createWrapper(document.body);
};
afterEach(() => {
@@ -25,23 +29,22 @@ describe('initGitlabVersionCheck', () => {
});
describe.each`
- description | fixture | finders | componentTexts
- ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]}
- ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
- ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
- ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]}
- ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_BATCH_TEXT]}
- ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_BATCH_TEXT, VERSION_BADGE_TEXT]}
+ description | fixture | finders | componentTexts
+ ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]}
+ ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]}
+ ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_PATCH_TEXT]}
+ ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, VERSION_BADGE_TEXT]}
+ ${'with security modal el'} | ${SECURITY_MODAL_FIXTURE} | ${[SECURITY_MODAL_FINDER]} | ${[SECURITY_MODAL_TEXT]}
+ ${'with security modal, security patch, and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${SECURITY_MODAL_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, SECURITY_MODAL_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, SECURITY_MODAL_TEXT, VERSION_BADGE_TEXT]}
`('$description', ({ fixture, finders, componentTexts }) => {
beforeEach(() => {
createApp(fixture);
});
it(`correctly renders the Version Check Components`, () => {
- const vueAppInstances = vueApps.map((v) => v && createWrapper(v));
- const renderedComponentTexts = vueAppInstances.map((v, index) =>
- v?.find(finders[index]).text(),
- );
+ const renderedComponentTexts = finders.map((f) => wrapper.find(f)?.element?.innerText.trim());
expect(renderedComponentTexts).toStrictEqual(componentTexts);
});
diff --git a/spec/frontend/gitlab_version_check/mock_data.js b/spec/frontend/gitlab_version_check/mock_data.js
index 6f2107f4d3c..707d45550eb 100644
--- a/spec/frontend/gitlab_version_check/mock_data.js
+++ b/spec/frontend/gitlab_version_check/mock_data.js
@@ -13,4 +13,10 @@ export const SECURITY_PATCH_FIXTURE = `<div id="js-security-patch-upgrade-alert"
export const SECURITY_PATCH_FINDER = 'h2';
-export const SECURITY_BATCH_TEXT = 'Critical security upgrade available';
+export const SECURITY_PATCH_TEXT = 'Critical security upgrade available';
+
+export const SECURITY_MODAL_FIXTURE = `<div id="js-security-patch-upgrade-alert-modal" data-current-version="15.1" data-version='{ "details": "test details", "latest-stable-versions": "[]" }'></div>`;
+
+export const SECURITY_MODAL_FINDER = '[data-testid="alert-modal-title"]';
+
+export const SECURITY_MODAL_TEXT = 'Important notice - Critical security release';
diff --git a/spec/frontend/gitlab_version_check/utils_spec.js b/spec/frontend/gitlab_version_check/utils_spec.js
new file mode 100644
index 00000000000..6126d88dfec
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/utils_spec.js
@@ -0,0 +1,35 @@
+import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '~/gitlab_version_check/utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from '~/gitlab_version_check/constants';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ parseBoolean: jest.fn().mockReturnValue(true),
+ getCookie: jest.fn().mockReturnValue('true'),
+ setCookie: jest.fn(),
+}));
+
+describe('GitLab Version Check Utils', () => {
+ describe('setHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion and sets Cookie to `true`', () => {
+ const currentVersion = '99.9.9';
+
+ setHideAlertModalCookie(currentVersion);
+
+ expect(setCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`, true, {
+ expires: COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('getHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion, fetches said Cooke, and parsesBoolean it', () => {
+ const currentVersion = '99.9.9';
+
+ const res = getHideAlertModalCookie(currentVersion);
+
+ expect(getCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`);
+ expect(parseBoolean).toHaveBeenCalledWith('true');
+ expect(res).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 2a347892973..30475b36561 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -575,7 +575,7 @@ describe('WorkItemDetail component', () => {
`('$description', async ({ milestoneWidgetPresent, exists }) => {
const response = workItemResponseFactory({ milestoneWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler, workItemsMvcEnabled: true });
+ createComponent({ handler });
await waitForPromises();
expect(findWorkItemMilestone().exists()).toBe(exists);
diff --git a/spec/graphql/types/ci/freeze_period_status_enum_spec.rb b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb
new file mode 100644
index 00000000000..2d9c7071392
--- /dev/null
+++ b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiFreezePeriodStatus'], feature_category: :release_orchestration do
+ it 'exposes all freeze period statuses' do
+ expect(described_class.values.keys).to contain_exactly(*%w[ACTIVE INACTIVE])
+ end
+end
diff --git a/spec/graphql/types/ci/freeze_period_type_spec.rb b/spec/graphql/types/ci/freeze_period_type_spec.rb
new file mode 100644
index 00000000000..ff61eadae60
--- /dev/null
+++ b/spec/graphql/types/ci/freeze_period_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiFreezePeriod'], feature_category: :release_orchestration do
+ specify { expect(described_class.graphql_name).to eq('CiFreezePeriod') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ status start_cron end_cron start_time end_time
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_freeze_period) }
+end
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index a891a5c285b..4471735876a 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'includes the expected fields' do
expected_fields = %w[
name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments
- slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment
+ slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment deployFreezes
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
index 86d672067a3..0ffa04e851b 100644
--- a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
+++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) }
let(:public_key) { private_key.public_key }
let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
+ let(:jira_connect_proxy_url_setting) { nil }
let(:install_keys_url) { "#{stub_asymmetric_jwt_cdn}/#{public_key_id}" }
let(:qsh) do
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test')
@@ -25,6 +26,8 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
before do
stub_request(:get, install_keys_url)
.to_return(body: public_key.to_s, status: 200)
+
+ stub_application_setting(jira_connect_proxy_url: jira_connect_proxy_url_setting)
end
it 'returns true when verified with public key from CDN' do
@@ -89,10 +92,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
context 'with jira_connect_proxy_url setting' do
let(:stub_asymmetric_jwt_cdn) { 'https://example.com/-/jira_connect/public_keys' }
-
- before do
- stub_application_setting(jira_connect_proxy_url: 'https://example.com')
- end
+ let(:jira_connect_proxy_url_setting) { 'https://example.com' }
it 'requests the settings CDN' do
expect(JWT).to receive(:decode).twice.and_call_original
@@ -101,22 +101,6 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
expect(WebMock).to have_requested(:get, "https://example.com/-/jira_connect/public_keys/#{public_key_id}")
end
-
- context 'when jira_connect_oauth_self_managed disabled' do
- let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
-
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it 'requests the default CDN' do
- expect(JWT).to receive(:decode).twice.and_call_original
-
- expect(asymmetric_jwt).to be_valid
-
- expect(WebMock).to have_requested(:get, install_keys_url)
- end
- end
end
end
diff --git a/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb
new file mode 100644
index 00000000000..b2f298a7d05
--- /dev/null
+++ b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::WorkItems::WorkItemHierarchy, feature_category: :portfolio_management do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) }
+ let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:ignored1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:ignored2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item2, work_item: item3) }
+ let_it_be(:link3) { create(:parent_link, work_item_parent: item3, work_item: item4) }
+
+ let(:options) { {} }
+
+ describe '#base_and_ancestors' do
+ subject { described_class.new(::WorkItem.where(id: item3.id), options: options) }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_ancestors
+
+ expect(relation).to eq([item3, item2, item1])
+ end
+
+ context 'when same_type option is used' do
+ let(:options) { { same_type: true } }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_ancestors
+
+ expect(relation).to eq([item3, item2])
+ end
+ end
+
+ it 'can find ancestors upto a certain level' do
+ relation = subject.base_and_ancestors(upto: item1)
+
+ expect(relation).to eq([item3, item2])
+ end
+
+ describe 'hierarchy_order option' do
+ let(:relation) do
+ subject.base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ context 'for :asc' do
+ let(:hierarchy_order) { :asc }
+
+ it 'orders by child to ancestor' do
+ expect(relation).to eq([item3, item2, item1])
+ end
+ end
+
+ context 'for :desc' do
+ let(:hierarchy_order) { :desc }
+
+ it 'orders by ancestor to child' do
+ expect(relation).to eq([item1, item2, item3])
+ end
+ end
+ end
+ end
+
+ describe '#base_and_descendants' do
+ subject { described_class.new(::WorkItem.where(id: item2.id), options: options) }
+
+ it 'includes the base and its descendants' do
+ relation = subject.base_and_descendants
+
+ expect(relation).to eq([item2, item3, item4])
+ end
+
+ context 'when same_type option is used' do
+ let(:options) { { same_type: true } }
+
+ it 'includes the base and its ancestors' do
+ relation = subject.base_and_descendants
+
+ expect(relation).to eq([item2, item3])
+ end
+ end
+
+ context 'when with_depth is true' do
+ let(:relation) do
+ subject.base_and_descendants(with_depth: true)
+ end
+
+ it 'includes depth in the results' do
+ object_depths = {
+ item2.id => 1,
+ item3.id => 2,
+ item4.id => 3
+ }
+
+ relation.each do |object|
+ expect(object.depth).to eq(object_depths[object.id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
index b9bf1657e28..d8add736d6a 100644
--- a/spec/models/ci/freeze_period_spec.rb
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -2,16 +2,22 @@
require 'spec_helper'
-RSpec.describe Ci::FreezePeriod, type: :model do
+RSpec.describe Ci::FreezePeriod, feature_category: :release_orchestration, type: :model do
+ let_it_be(:project) { create(:project) }
+
+ # Freeze period factory is on a weekend, so we travel in time, in and around that.
+ let(:friday_2300_time) { Time.utc(2020, 4, 10, 23, 0) }
+ let(:saturday_1200_time) { Time.utc(2020, 4, 11, 12, 0) }
+ let(:monday_0700_time) { Time.utc(2020, 4, 13, 7, 0) }
+ let(:tuesday_0800_time) { Time.utc(2020, 4, 14, 8, 0) }
+
subject { build(:ci_freeze_period) }
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
- let!(:model) { create(:ci_freeze_period, project: parent) }
+ let!(:model) { create(:ci_freeze_period, project: parent) }
end
- let(:invalid_cron) { '0 0 0 * *' }
-
it { is_expected.to belong_to(:project) }
it { is_expected.to respond_to(:freeze_start) }
@@ -19,37 +25,142 @@ RSpec.describe Ci::FreezePeriod, type: :model do
it { is_expected.to respond_to(:cron_timezone) }
describe 'cron validations' do
+ let(:invalid_cron) { '0 0 0 * *' }
+
it 'allows valid cron patterns' do
- freeze_period = build(:ci_freeze_period)
+ freeze_period = build_stubbed(:ci_freeze_period)
expect(freeze_period).to be_valid
end
it 'does not allow invalid cron patterns on freeze_start' do
- freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_start: invalid_cron)
expect(freeze_period).not_to be_valid
end
it 'does not allow invalid cron patterns on freeze_end' do
- freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_end: invalid_cron)
expect(freeze_period).not_to be_valid
end
it 'does not allow an invalid timezone' do
- freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
+ freeze_period = build_stubbed(:ci_freeze_period, cron_timezone: 'invalid')
expect(freeze_period).not_to be_valid
end
context 'when cron contains trailing whitespaces' do
it 'strips the attribute' do
- freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
+ freeze_period = build_stubbed(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
expect(freeze_period).to be_valid
expect(freeze_period.freeze_start).to eq('0 0 * * *')
end
end
end
+
+ shared_examples 'within freeze period' do |time|
+ it 'is frozen' do
+ travel_to(time) do
+ expect(subject).to eq(Ci::FreezePeriod::STATUS_ACTIVE)
+ end
+ end
+ end
+
+ shared_examples 'outside freeze period' do |time|
+ it 'is not frozen' do
+ travel_to(time) do
+ expect(subject).to eq(Ci::FreezePeriod::STATUS_INACTIVE)
+ end
+ end
+ end
+
+ describe '#status' do
+ subject { freeze_period.status }
+
+ describe 'single freeze period' do
+ let(:freeze_period) do
+ build_stubbed(:ci_freeze_period, project: project)
+ end
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/370472
+ context 'when period overlaps with itself' do
+ let(:freeze_period) do
+ build_stubbed(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *')
+ end
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0)
+ it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0)
+ end
+ end
+
+ shared_examples 'a freeze period method' do
+ let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) }
+
+ it 'returns the correct value' do
+ travel_to(now) do
+ expect(freeze_period.send(method)).to eq(expected)
+ end
+ end
+ end
+
+ describe '#active?' do
+ context 'when freeze period status is active' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :active? }
+ let(:expected) { true }
+ end
+ end
+
+ context 'when freeze period status is inactive' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { tuesday_0800_time }
+ let(:method) { :active? }
+ let(:expected) { false }
+ end
+ end
+ end
+
+ describe '#time_start' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { monday_0700_time }
+ let(:method) { :time_start }
+ let(:expected) { friday_2300_time }
+ end
+ end
+
+ describe '#next_time_start' do
+ let(:next_friday_2300_time) { Time.utc(2020, 4, 17, 23, 0) }
+
+ it_behaves_like 'a freeze period method' do
+ let(:now) { monday_0700_time }
+ let(:method) { :next_time_start }
+ let(:expected) { next_friday_2300_time }
+ end
+ end
+
+ describe '#time_end_from_now' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :time_end_from_now }
+ let(:expected) { monday_0700_time }
+ end
+ end
+
+ describe '#time_end_from_start' do
+ it_behaves_like 'a freeze period method' do
+ let(:now) { saturday_1200_time }
+ let(:method) { :time_end_from_start }
+ let(:expected) { monday_0700_time }
+ end
+ end
end
diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb
deleted file mode 100644
index ecbb7af64f7..00000000000
--- a/spec/models/ci/freeze_period_status_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Ci::FreezePeriodStatus do
- let(:project) { create :project }
- # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
- let(:friday_2300) { '0 23 * * 5' }
- let(:monday_0700) { '0 7 * * 1' }
-
- subject { described_class.new(project: project).execute }
-
- shared_examples 'within freeze period' do |time|
- it 'is frozen' do
- travel_to(time) do
- expect(subject).to be_truthy
- end
- end
- end
-
- shared_examples 'outside freeze period' do |time|
- it 'is not frozen' do
- travel_to(time) do
- expect(subject).to be_falsy
- end
- end
- end
-
- describe 'single freeze period' do
- let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
- end
-
- describe 'multiple freeze periods' do
- # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday.""
- let(:friday_2330) { '30 23 * * 5' }
- let(:monday_0800) { '0 8 * * 1' }
-
- let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
- let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) }
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
-
- it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
- end
-
- # https://gitlab.com/gitlab-org/gitlab/-/issues/370472
- context 'when period overlaps with itself' do
- let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') }
-
- it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0)
-
- it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0)
- end
-end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index cc48c627049..2670127442e 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -2010,4 +2010,23 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#deploy_freezes' do
+ let(:environment) { create(:environment, project: project, name: 'staging') }
+ let(:freeze_period) { create(:ci_freeze_period, project: project) }
+
+ subject { environment.deploy_freezes }
+
+ it 'returns the freeze periods of the associated project' do
+ expect(subject).to contain_exactly(freeze_period)
+ end
+
+ it 'caches the freeze periods' do
+ expect(Gitlab::SafeRequestStore).to receive(:fetch)
+ .at_least(:once)
+ .and_return([freeze_period])
+
+ subject
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9a4179b9157..04edb755b58 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -8482,6 +8482,41 @@ RSpec.describe Project, factory_default: :keep do
it_behaves_like 'cascading settings', :only_allow_merge_if_all_discussions_are_resolved
end
+ describe '#archived' do
+ it { expect(subject.archived).to be_falsey }
+ it { expect(described_class.new(archived: true).archived).to be_truthy }
+ end
+
+ describe '#resolve_outdated_diff_discussions' do
+ it { expect(subject.resolve_outdated_diff_discussions).to be_falsey }
+
+ context 'when set explicitly' do
+ subject { described_class.new(resolve_outdated_diff_discussions: true) }
+
+ it { expect(subject.resolve_outdated_diff_discussions).to be_truthy }
+ end
+ end
+
+ describe '#only_allow_merge_if_all_discussions_are_resolved' do
+ it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_falsey }
+
+ context 'when set explicitly' do
+ subject { described_class.new(only_allow_merge_if_all_discussions_are_resolved: true) }
+
+ it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_truthy }
+ end
+ end
+
+ describe '#remove_source_branch_after_merge' do
+ it { expect(subject.remove_source_branch_after_merge).to be_truthy }
+
+ context 'when set explicitly' do
+ subject { described_class.new(remove_source_branch_after_merge: false) }
+
+ it { expect(subject.remove_source_branch_after_merge).to be_falsey }
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 341f9a9c60f..1c34936c5c2 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItem do
+RSpec.describe WorkItem, feature_category: :portfolio_management do
let_it_be(:reusable_project) { create(:project) }
describe 'associations' do
@@ -176,4 +176,59 @@ RSpec.describe WorkItem do
end
end
end
+
+ context 'with hierarchy' do
+ let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:type3) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:type4) { create(:work_item_type, namespace: reusable_project.namespace) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) }
+ let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type3) }
+ let_it_be(:hierarchy_restriction4) { create(:hierarchy_restriction, parent_type: type3, child_type: type3) }
+ let_it_be(:hierarchy_restriction5) { create(:hierarchy_restriction, parent_type: type3, child_type: type4) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: reusable_project) }
+ let_it_be(:item2_1) { create(:work_item, work_item_type: type2, project: reusable_project) }
+ let_it_be(:item2_2) { create(:work_item, work_item_type: type2, project: reusable_project) }
+ let_it_be(:item3_1) { create(:work_item, work_item_type: type3, project: reusable_project) }
+ let_it_be(:item3_2) { create(:work_item, work_item_type: type3, project: reusable_project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type4, project: reusable_project) }
+ let_it_be(:ignored_ancestor) { create(:work_item, work_item_type: type1, project: reusable_project) }
+ let_it_be(:ignored_descendant) { create(:work_item, work_item_type: type4, project: reusable_project) }
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2_1) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item2_1, work_item: item2_2) }
+ let_it_be(:link3) { create(:parent_link, work_item_parent: item2_2, work_item: item3_1) }
+ let_it_be(:link4) { create(:parent_link, work_item_parent: item3_1, work_item: item3_2) }
+ let_it_be(:link5) { create(:parent_link, work_item_parent: item3_2, work_item: item4) }
+
+ describe '#ancestors' do
+ it 'returns all ancestors in ascending order' do
+ expect(item3_1.ancestors).to eq([item2_2, item2_1, item1])
+ end
+
+ it 'returns an empty array if there are no ancestors' do
+ expect(item1.ancestors).to be_empty
+ end
+ end
+
+ describe '#same_type_base_and_ancestors' do
+ it 'returns self and all ancestors of the same type in ascending order' do
+ expect(item3_2.same_type_base_and_ancestors).to eq([item3_2, item3_1])
+ end
+
+ it 'returns self if there are no ancestors of the same type' do
+ expect(item3_1.same_type_base_and_ancestors).to match_array([item3_1])
+ end
+ end
+
+ describe '#same_type_descendants_depth' do
+ it 'returns max descendants depth including self' do
+ expect(item3_1.same_type_descendants_depth).to eq(2)
+ end
+
+ it 'returns 1 if there are no descendants' do
+ expect(item1.same_type_descendants_depth).to eq(1)
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
index a2e6ba06199..f6d325c62e7 100644
--- a/spec/models/work_items/parent_link_spec.rb
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -89,6 +89,55 @@ RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do
end
end
+ context 'with nested ancestors' do
+ let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) }
+ let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) }
+ let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:item4) { create(:work_item, work_item_type: type2, project: project) }
+ let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) }
+ let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) }
+
+ let_it_be(:hierarchy_restriction3) do
+ create(:hierarchy_restriction, parent_type: type2, child_type: type2, maximum_depth: 2)
+ end
+
+ let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: item3, work_item: item4) }
+
+ describe '#validate_depth' do
+ it 'is valid if depth is in limit' do
+ link = build(:parent_link, work_item_parent: item1, work_item: item3)
+
+ expect(link).to be_valid
+ end
+
+ it 'is not valid when maximum depth is reached' do
+ link = build(:parent_link, work_item_parent: item2, work_item: item3)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item]).to include('reached maximum depth')
+ end
+ end
+
+ describe '#validate_cyclic_reference' do
+ it 'is not valid if parent and child are same' do
+ link1.work_item_parent = item2
+
+ expect(link1).not_to be_valid
+ expect(link1.errors[:work_item]).to include('is not allowed to point to itself')
+ end
+
+ it 'is not valid if child is already in ancestors' do
+ link = build(:parent_link, work_item_parent: item4, work_item: item3)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item]).to include('is already present in ancestors')
+ end
+ end
+ end
+
it 'is not valid if parent is in other project' do
link = build(:parent_link, work_item_parent: task1, work_item: build(:work_item))
diff --git a/spec/presenters/ci/freeze_period_presenter_spec.rb b/spec/presenters/ci/freeze_period_presenter_spec.rb
new file mode 100644
index 00000000000..e9959540b8d
--- /dev/null
+++ b/spec/presenters/ci/freeze_period_presenter_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::FreezePeriodPresenter, feature_category: :release_orchestration do
+ let_it_be(:project) { build_stubbed(:project) }
+
+ let(:presenter) { described_class.new(freeze_period) }
+
+ describe '#start_time' do
+ let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) }
+
+ context 'when active' do
+ # Default freeze period factory is on a weekend, so let's travel in time to a Saturday!
+ let(:time) { Time.utc(2022, 12, 3, 6) }
+ let(:previous_start) { Time.utc(2022, 12, 2, 23) }
+
+ it 'returns the previous time of the freeze period start' do
+ travel_to(time) do
+ expect(presenter.start_time).to eq(previous_start)
+ end
+ end
+ end
+
+ context 'when inactive' do
+ # Default freeze period factory is on a weekend, so we travel back a couple of days earlier.
+ let(:time) { Time.utc(2022, 11, 30, 6) }
+ let(:next_start) { Time.utc(2022, 12, 2, 23) }
+
+ it 'returns the next time of the freeze period start' do
+ travel_to(time) do
+ expect(presenter.start_time).to eq(next_start)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 8e4b905791c..99eef3564a4 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -93,6 +93,11 @@ RSpec.describe 'getting a work item list for a project' do
}
... on WorkItemWidgetHierarchy {
parent { id }
+ children {
+ nodes {
+ id
+ }
+ }
}
... on WorkItemWidgetLabels {
labels { nodes { id } }
diff --git a/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb
index 942e27f2559..4387a3f5b07 100644
--- a/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb
+++ b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb
@@ -12,5 +12,9 @@ RSpec.describe 'shared/gitlab_version/_security_patch_upgrade_alert' do
it 'renders the security patch upgrade alert' do
expect(rendered).to have_selector('#js-security-patch-upgrade-alert')
end
+
+ it 'renders the security patch upgrade alert modal' do
+ expect(rendered).to have_selector('#js-security-patch-upgrade-alert-modal')
+ end
end
end