diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 522c3806095..ff24873ca4c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-40fae4205d3ad62ca9341620146486bee8d31b28
+d924490032231edb9452acdaca7d8e4747cf6ab4
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84a5d5ae4b3..01e463c1965 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -870,6 +870,14 @@ const Api = {
return axios.put(url, freezePeriod);
},
+ deleteFreezePeriod(id, freezePeriodId) {
+ const url = Api.buildUrl(this.freezePeriodPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':freeze_period_id', encodeURIComponent(freezePeriodId));
+
+ return axios.delete(url);
+ },
+
trackRedisCounterEvent(event) {
if (!gon.features?.usageDataApi) {
return null;
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
index 8282f1d910a..77767456f76 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
@@ -1,5 +1,5 @@
@@ -72,6 +100,18 @@ export default {
@click="setFreezePeriod(item)"
/>
+
+
+
@@ -90,5 +130,24 @@ export default {
>
{{ $options.translations.addDeployFreeze }}
+
+
+
+
+ {{ freezePeriodToDelete.freezeStart }}
+
+
+ {{ freezePeriodToDelete.freezeEnd }}
+
+ {{ freezePeriodToDelete.cronTimezone.formattedTimezone }}
+
+
+
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index fed80b46eda..a2056165cb2 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -52,6 +52,22 @@ export const updateFreezePeriod = (store) =>
}),
);
+export const deleteFreezePeriod = ({ state, commit }, { id }) => {
+ commit(types.REQUEST_DELETE_FREEZE_PERIOD, id);
+
+ return Api.deleteFreezePeriod(state.projectId, id)
+ .then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
+ .catch((e) => {
+ createFlash({
+ message: __('Error: Unable to delete deploy freeze'),
+ });
+ commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
+
+ // eslint-disable-next-line no-console
+ console.error('[gitlab] Unable to delete deploy freeze:', e);
+ });
+};
+
export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.REQUEST_FREEZE_PERIODS);
diff --git a/app/assets/javascripts/deploy_freeze/store/mutation_types.js b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
index 8e6fdfd4443..0fec96e2e4c 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutation_types.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
@@ -10,4 +10,8 @@ export const SET_SELECTED_ID = 'SET_SELECTED_ID';
export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
+export const REQUEST_DELETE_FREEZE_PERIOD = 'REQUEST_DELETE_FREEZE_PERIOD';
+export const RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS = 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS';
+export const RECEIVE_DELETE_FREEZE_PERIOD_ERROR = 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR';
+
export const RESET_MODAL = 'RESET_MODAL';
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
index fdd1ea6e32e..151f7f39f5a 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutations.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -1,15 +1,28 @@
+import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { secondsToHours } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
-const formatTimezoneName = (freezePeriod, timezoneList) =>
- convertObjectPropsToCamelCase({
+const formatTimezoneName = (freezePeriod, timezoneList) => {
+ const tz = timezoneList.find((timezone) => timezone.identifier === freezePeriod.cron_timezone);
+ return convertObjectPropsToCamelCase({
...freezePeriod,
cron_timezone: {
- formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)
- ?.name,
+ formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
identifier: freezePeriod.cron_timezone,
},
});
+};
+
+const setFreezePeriodIsDeleting = (state, id, isDeleting) => {
+ const freezePeriod = state.freezePeriods.find((f) => f.id === id);
+
+ if (!freezePeriod) {
+ return;
+ }
+
+ Vue.set(freezePeriod, 'isDeleting', isDeleting);
+};
export default {
[types.REQUEST_FREEZE_PERIODS](state) {
@@ -53,6 +66,18 @@ export default {
state.selectedId = id;
},
+ [types.REQUEST_DELETE_FREEZE_PERIOD](state, id) {
+ setFreezePeriodIsDeleting(state, id, true);
+ },
+
+ [types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS](state, id) {
+ state.freezePeriods = state.freezePeriods.filter((f) => f.id !== id);
+ },
+
+ [types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR](state, id) {
+ setFreezePeriodIsDeleting(state, id, false);
+ },
+
[types.RESET_MODAL](state) {
state.freezeStartCron = '';
state.freezeEndCron = '';
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 9e058af56c4..cec53869aa8 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
},
methods: {
onChangePage(page) {
@@ -42,7 +38,7 @@ export default {
-
+
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index 467c89fd8b8..d71b553a878 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -27,10 +27,6 @@ export default {
type: Object,
required: true,
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
canAdminEnvironment: {
type: Boolean,
required: true,
@@ -84,7 +80,7 @@ export default {
return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
},
shouldShowExternalUrlButton() {
- return this.canReadEnvironment && Boolean(this.environment.externalUrl);
+ return Boolean(this.environment.externalUrl);
},
shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable;
@@ -138,7 +134,7 @@ export default {
>{{ $options.i18n.externalButtonText }}
[],
},
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -155,7 +150,6 @@ export default {
@@ -191,7 +185,6 @@ export default {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 1be9a4608cb..206381e0b7e 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
import environmentsFolderApp from './environments_folder_view.vue';
@@ -31,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass,
- canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
};
},
render(createElement) {
@@ -40,7 +38,6 @@ export default () => {
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
- canReadEnvironment: this.canReadEnvironment,
},
});
},
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 8070f3f12f8..3c608ad0ba9 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -30,10 +30,6 @@ export default {
required: false,
default: '',
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
},
methods: {
successCallback(resp) {
@@ -72,7 +68,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index f1f8b4c8bc8..5e33923d518 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -32,7 +32,6 @@ export default () => {
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
@@ -42,7 +41,6 @@ export default () => {
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
canCreateEnvironment: this.canCreateEnvironment,
- canReadEnvironment: this.canReadEnvironment,
},
});
},
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index f1c2dfec94b..6df4fad83f2 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -36,7 +36,6 @@ export const initHeader = () => {
environment: this.environment,
canDestroyEnvironment: dataset.canDestroyEnvironment,
canUpdateEnvironment: dataset.canUpdateEnvironment,
- canReadEnvironment: dataset.canReadEnvironment,
canStopEnvironment: dataset.canStopEnvironment,
canAdminEnvironment: dataset.canAdminEnvironment,
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
diff --git a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
new file mode 100644
index 00000000000..83d36209bb3
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index 4e16b16041f..3b4f4eb51fe 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -6,6 +6,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
+ case 'image':
+ return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
default:
return null;
}
@@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => {
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
+ image: {
+ url: blob.rawPath,
+ alt: blob.name,
+ },
}[type];
};
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
index c553d5b14a0..07967434f37 100644
--- a/app/assets/javascripts/search/highlight_blob_search_result.js
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -2,7 +2,7 @@ export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
const searchTerm = search.toLowerCase();
- const blobs = contentBody.querySelectorAll('.blob-result');
+ const blobs = contentBody.querySelectorAll('.js-blob-result');
blobs.forEach((blob) => {
const lines = blob.querySelectorAll('.line');
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index f0f074792ed..7dea6191fa4 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -27,9 +27,7 @@ module Boards
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = issues_from(list_service)
- if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
- Issue.move_nulls_to_end(issues)
- end
+ ::Boards::Issues::ListService.initialize_relative_positions(board, current_user, issues)
render_issues(issues, list_service.metadata)
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3dffbbf3108..96432fa30c9 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -21,9 +21,10 @@ class RegistrationsController < Devise::RegistrationsController
def create
set_user_state
- accept_pending_invitations
super do |new_user|
+ accept_pending_invitations if new_user.persisted?
+
persist_accepted_terms_if_required(new_user)
set_role_required(new_user)
diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb
index 25fb35ec74b..4b09e753a18 100644
--- a/app/graphql/resolvers/board_list_issues_resolver.rb
+++ b/app/graphql/resolvers/board_list_issues_resolver.rb
@@ -15,8 +15,11 @@ module Resolvers
def resolve(**args)
filter_params = item_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
+ pagination_connections = Gitlab::Graphql::Pagination::Keyset::Connection.new(service.execute)
- service.execute
+ ::Boards::Issues::ListService.initialize_relative_positions(list.board, current_user, pagination_connections.items)
+
+ pagination_connections
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/235681
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
index 63357e2345b..7b35eab3d27 100644
--- a/app/graphql/types/ci/stage_type.rb
+++ b/app/graphql/types/ci/stage_type.rb
@@ -52,15 +52,22 @@ module Types
# rubocop: disable CodeReuse/ActiveRecord
def jobs_for_pipeline(pipeline, stage_ids, include_needs)
- builds_results = pipeline.latest_builds.where(stage_id: stage_ids).preload(:job_artifacts, :project)
- bridges_results = pipeline.bridges.where(stage_id: stage_ids).preload(:project)
- builds_results = builds_results.preload(:needs) if include_needs
- bridges_results = bridges_results.preload(:needs) if include_needs
- commit_status_results = pipeline.latest_statuses.where(stage_id: stage_ids)
+ jobs = pipeline.statuses.latest.where(stage_id: stage_ids)
- results = builds_results | bridges_results | commit_status_results
+ common_relations = [:project]
+ common_relations << :needs if include_needs
- results.group_by(&:stage_id)
+ preloaders = {
+ ::Ci::Build => [:metadata, :job_artifacts],
+ ::Ci::Bridge => [:metadata, :downstream_pipeline],
+ ::GenericCommitStatus => []
+ }
+
+ preloaders.each do |klass, relations|
+ ActiveRecord::Associations::Preloader.new.preload(jobs.select { |job| job.is_a?(klass) }, relations + common_relations)
+ end
+
+ jobs.group_by(&:stage_id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 3f23f73eed7..f57bb600527 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -73,7 +73,6 @@ module EnvironmentHelper
external_url: environment.external_url,
can_update_environment: can?(current_user, :update_environment, environment),
can_destroy_environment: can_destroy_environment?(environment),
- can_read_environment: can?(current_user, :read_environment, environment),
can_stop_environment: can?(current_user, :stop_environment, environment),
can_admin_environment: can?(current_user, :admin_environment, project),
environment_metrics_path: environment_metrics_path(environment),
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 308d36ce50f..71cec48aa4e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -66,6 +66,7 @@ module Ci
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :generic_commit_statuses, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus'
has_many :job_artifacts, through: :builds
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
index 48605ecc3d7..d5b86db2640 100644
--- a/app/models/concerns/cron_schedulable.rb
+++ b/app/models/concerns/cron_schedulable.rb
@@ -14,12 +14,10 @@ module CronSchedulable
# The `next_run_at` column is set to the actual execution date of worker that
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
- def calculate_next_run_at
- now = Time.zone.now
+ def calculate_next_run_at(start_time = Time.zone.now)
+ ideal_next_run = ideal_next_run_from(start_time)
- ideal_next_run = ideal_next_run_from(now)
-
- if ideal_next_run == cron_worker_next_run_from(now)
+ if ideal_next_run == cron_worker_next_run_from(start_time)
ideal_next_run
else
cron_worker_next_run_from(ideal_next_run)
diff --git a/app/models/member.rb b/app/models/member.rb
index 397e60be3a8..28b8695f70b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -278,12 +278,14 @@ class Member < ApplicationRecord
def accept_invite!(new_user)
return false unless invite?
+ return false unless new_user
+
+ self.user = new_user
+ return false unless self.user.save
self.invite_token = nil
self.invite_accepted_at = Time.current.utc
- self.user = new_user
-
saved = self.save
after_accept_invite if saved
diff --git a/app/models/note.rb b/app/models/note.rb
index 7619ace6dce..7f6f7db36c4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -582,7 +582,7 @@ class Note < ApplicationRecord
end
def post_processed_cache_key
- cache_key_items = [cache_key, author.cache_key]
+ cache_key_items = [cache_key, author&.cache_key]
cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
cache_key_items.join(':')
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 384cb3285fc..06ed6791bb7 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -12,11 +12,11 @@ module Ci
erased_by.name if erased_by_user?
end
- def status_title
+ def status_title(status = detailed_status)
if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
else
- tooltip_for_badge
+ tooltip_for_badge(status)
end
end
@@ -41,8 +41,8 @@ module Ci
private
- def tooltip_for_badge
- detailed_status.badge_tooltip.capitalize
+ def tooltip_for_badge(status)
+ status.badge_tooltip.capitalize
end
def detailed_status
diff --git a/app/presenters/ci/stage_presenter.rb b/app/presenters/ci/stage_presenter.rb
index 9ec3f8d153a..2d5ee3d2f25 100644
--- a/app/presenters/ci/stage_presenter.rb
+++ b/app/presenters/ci/stage_presenter.rb
@@ -15,18 +15,23 @@ module Ci
private
def preload_statuses(statuses)
- loaded_statuses = statuses.load
- statuses.tap do |statuses|
- # rubocop: disable CodeReuse/ActiveRecord
- ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata])
- # rubocop: enable CodeReuse/ActiveRecord
- end
- end
+ common_relations = [:pipeline]
- def preloadable_statuses(statuses)
- statuses.reject do |status|
- status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge)
+ preloaders = {
+ ::Ci::Build => [:metadata, :tags, :job_artifacts_archive],
+ ::Ci::Bridge => [:metadata, :downstream_pipeline],
+ ::GenericCommitStatus => []
+ }
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ preloaders.each do |klass, relations|
+ ActiveRecord::Associations::Preloader
+ .new
+ .preload(statuses.select { |job| job.is_a?(klass) }, relations + common_relations)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ statuses
end
end
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 8806e6788ff..9a3e3bc3bdb 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -9,6 +9,14 @@ module Boards
IssuesFinder.valid_params
end
+ # It is a class method because we cannot apply it
+ # prior to knowing how many items should be fetched for a list.
+ def self.initialize_relative_positions(board, current_user, issues)
+ if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
+ Issue.move_nulls_to_end(issues)
+ end
+ end
+
private
def order(items)
diff --git a/app/validators/json_schemas/dast_profile_schedule_cadence.json b/app/validators/json_schemas/dast_profile_schedule_cadence.json
new file mode 100644
index 00000000000..5583acfa5af
--- /dev/null
+++ b/app/validators/json_schemas/dast_profile_schedule_cadence.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Dast profile schedule cadence schema",
+ "type": "object",
+ "anyOf": [
+ {
+ "properties": {
+ "unit": { "enum": ["day"] },
+ "duration": { "enum": [1] }
+ }
+ },
+ {
+ "properties": {
+ "unit": { "enum": ["week"] },
+ "duration": { "enum": [1] }
+ }
+ },
+ {
+ "properties": {
+ "unit": { "enum": ["month"] },
+ "duration": { "enum": [1, 3 ,6] }
+ }
+ },
+ {
+ "properties": {
+ "unit": { "enum": ["year"] },
+ "duration": { "enum": [1] }
+ }
+ }
+ ]
+}
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 1f45f3eae26..59523ed3a0c 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -52,7 +52,7 @@
.float-right
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id
- = f.submit _('Enable'), class: 'gl-button btn btn-sm'
+ = f.submit _('Enable'), class: 'gl-button btn btn-sm', data: { confirm: (s_('Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?') if @runner.instance_type?) }
= paginate_without_count @projects
.col-md-6
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 1a3813ba99f..437529c3608 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -7,10 +7,14 @@
- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
+-# This prevents initializing another Ci::Status object where 'status' is used
+- status = job.detailed_status(current_user)
%tr.build.commit{ class: ('retried' if retried) }
%td.status
- = render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
+ -# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
+ -# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
+ = render "ci/status/badge", status: status, title: job.status_title(status)
%td
- if can?(current_user, :read_build, job)
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index 2f13d7d96c7..dcbf2c02307 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -1,4 +1,4 @@
-.blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
+.js-blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
.file-holder.file-holder-top-border
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
= link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
diff --git a/config/feature_flags/development/method_instrumentation_disable_initialization.yml b/config/feature_flags/development/method_instrumentation_disable_initialization.yml
new file mode 100644
index 00000000000..d73d6fdaac7
--- /dev/null
+++ b/config/feature_flags/development/method_instrumentation_disable_initialization.yml
@@ -0,0 +1,8 @@
+---
+name: method_instrumentation_disable_initialization
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69091
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339665
+milestone: '14.3'
+type: development
+group: group::memory
+default_enabled: false
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index e352ff5090a..bb768070b1f 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -178,27 +178,33 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
)
- Gitlab::Metrics::Instrumentation
- .instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
- # Instrumenting the ApplicationSetting class can lead to an infinite
- # loop. Since the data is cached any way we don't really need to
- # instrument it.
- if klass == ApplicationSetting
- false
- else
- loc = method.source_location
+ # We are removing the Instrumentation module entirely in steps.
+ # More in https://gitlab.com/gitlab-org/gitlab/-/issues/217978.
+ unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
+ Gitlab::Metrics::Instrumentation
+ .instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
+ # Instrumenting the ApplicationSetting class can lead to an infinite
+ # loop. Since the data is cached any way we don't really need to
+ # instrument it.
+ if klass == ApplicationSetting
+ false
+ else
+ loc = method.source_location
- loc && loc[0].start_with?(models_path) && method.source =~ regex
+ loc && loc[0].start_with?(models_path) && method.source =~ regex
+ end
end
- end
- # Ability is in app/models, is not an ActiveRecord model, but should still
- # be instrumented.
- Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
+ # Ability is in app/models, is not an ActiveRecord model, but should still
+ # be instrumented.
+ Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
+ end
end
- Gitlab::Metrics::Instrumentation.configure do |config|
- instrument_classes(config)
+ unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
+ Gitlab::Metrics::Instrumentation.configure do |config|
+ instrument_classes(config)
+ end
end
GC::Profiler.enable
diff --git a/db/migrate/20210807101446_add_cadence_to_dast_profile_schedules.rb b/db/migrate/20210807101446_add_cadence_to_dast_profile_schedules.rb
new file mode 100644
index 00000000000..c9b17e3d5c5
--- /dev/null
+++ b/db/migrate/20210807101446_add_cadence_to_dast_profile_schedules.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddCadenceToDastProfileSchedules < ActiveRecord::Migration[6.1]
+ def change
+ add_column :dast_profile_schedules, :cadence, :jsonb, null: false, default: {}
+ end
+end
diff --git a/db/migrate/20210807101621_add_timezone_to_dast_profile_schedules.rb b/db/migrate/20210807101621_add_timezone_to_dast_profile_schedules.rb
new file mode 100644
index 00000000000..3c3eb507432
--- /dev/null
+++ b/db/migrate/20210807101621_add_timezone_to_dast_profile_schedules.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class AddTimezoneToDastProfileSchedules < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # We disable these cops here because adding the column is safe. The table does not
+ # have any data in it as it's behind a feature flag.
+ # rubocop: disable Rails/NotNullColumn
+ def up
+ execute('DELETE FROM dast_profile_schedules')
+
+ unless column_exists?(:dast_profile_schedules, :timezone)
+ add_column :dast_profile_schedules, :timezone, :text, null: false
+ end
+
+ add_text_limit :dast_profile_schedules, :timezone, 255
+ end
+
+ def down
+ return unless column_exists?(:dast_profile_schedules, :timezone)
+
+ remove_column :dast_profile_schedules, :timezone
+ end
+end
diff --git a/db/migrate/20210807102004_add_starts_at_to_dast_profile_schedules.rb b/db/migrate/20210807102004_add_starts_at_to_dast_profile_schedules.rb
new file mode 100644
index 00000000000..4eea5fd7e8c
--- /dev/null
+++ b/db/migrate/20210807102004_add_starts_at_to_dast_profile_schedules.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStartsAtToDastProfileSchedules < ActiveRecord::Migration[6.1]
+ def change
+ add_column :dast_profile_schedules, :starts_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
+ end
+end
diff --git a/db/migrate/20210816095826_add_unique_index_on_dast_profile_to_dast_profile_schedules.rb b/db/migrate/20210816095826_add_unique_index_on_dast_profile_to_dast_profile_schedules.rb
new file mode 100644
index 00000000000..b7ea8545df1
--- /dev/null
+++ b/db/migrate/20210816095826_add_unique_index_on_dast_profile_to_dast_profile_schedules.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUniqueIndexOnDastProfileToDastProfileSchedules < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_dast_profile_schedules_on_dast_profile_id'
+ TABLE = :dast_profile_schedules
+ # We disable these cops here because changing this index is safe. The table does not
+ # have any data in it as it's behind a feature flag.
+ # rubocop: disable Migration/AddIndex
+ # rubocop: disable Migration/RemoveIndex
+ def up
+ execute('DELETE FROM dast_profile_schedules')
+
+ if index_exists_by_name?(TABLE, INDEX_NAME)
+ remove_index TABLE, :dast_profile_id, name: INDEX_NAME
+ end
+
+ unless index_exists_by_name?(TABLE, INDEX_NAME)
+ add_index TABLE, :dast_profile_id, unique: true, name: INDEX_NAME
+ end
+ end
+
+ def down
+ execute('DELETE FROM dast_profile_schedules')
+
+ if index_exists_by_name?(TABLE, INDEX_NAME)
+ remove_index TABLE, :dast_profile_id, name: INDEX_NAME
+ end
+
+ unless index_exists_by_name?(TABLE, INDEX_NAME)
+ add_index TABLE, :dast_profile_id
+ end
+ end
+end
diff --git a/db/migrate/20210818061156_remove_project_profile_compound_index_from_dast_profile_schedules.rb b/db/migrate/20210818061156_remove_project_profile_compound_index_from_dast_profile_schedules.rb
new file mode 100644
index 00000000000..b50947a0a99
--- /dev/null
+++ b/db/migrate/20210818061156_remove_project_profile_compound_index_from_dast_profile_schedules.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveProjectProfileCompoundIndexFromDastProfileSchedules < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ TABLE = :dast_profile_schedules
+ INDEX_NAME = 'index_dast_profile_schedules_on_project_id_and_dast_profile_id'
+ # We disable these cops here because changing this index is safe. The table does not
+ # have any data in it as it's behind a feature flag.
+ # rubocop: disable Migration/AddIndex
+ # rubocop: disable Migration/RemoveIndex
+ def up
+ execute('DELETE FROM dast_profile_schedules')
+
+ if index_exists_by_name?(TABLE, INDEX_NAME)
+ remove_index TABLE, %i[project_id dast_profile_id], name: INDEX_NAME
+ end
+ end
+
+ def down
+ execute('DELETE FROM dast_profile_schedules')
+
+ unless index_exists_by_name?(TABLE, INDEX_NAME)
+ add_index TABLE, %i[project_id dast_profile_id], unique: true, name: INDEX_NAME
+ end
+ end
+end
diff --git a/db/migrate/20210818115613_add_index_project_id_on_dast_profile_schedule.rb b/db/migrate/20210818115613_add_index_project_id_on_dast_profile_schedule.rb
new file mode 100644
index 00000000000..392b335ab45
--- /dev/null
+++ b/db/migrate/20210818115613_add_index_project_id_on_dast_profile_schedule.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexProjectIdOnDastProfileSchedule < ActiveRecord::Migration[6.1]
+ # We disable these cops here because changing this index is safe. The table does not
+ # have any data in it as it's behind a feature flag.
+ # rubocop: disable Migration/AddIndex
+ def change
+ add_index :dast_profile_schedules, :project_id
+ end
+end
diff --git a/db/schema_migrations/20210807101446 b/db/schema_migrations/20210807101446
new file mode 100644
index 00000000000..0b6d526429f
--- /dev/null
+++ b/db/schema_migrations/20210807101446
@@ -0,0 +1 @@
+30e1463616c60b92afb28bbb76e3c55830a385af6df0e60e16ed96d9e75943b9
\ No newline at end of file
diff --git a/db/schema_migrations/20210807101621 b/db/schema_migrations/20210807101621
new file mode 100644
index 00000000000..ab053cf4cbc
--- /dev/null
+++ b/db/schema_migrations/20210807101621
@@ -0,0 +1 @@
+7e9b39914ade766357751953a4981225dbae7e5d371d4824af61b01af70f46ae
\ No newline at end of file
diff --git a/db/schema_migrations/20210807102004 b/db/schema_migrations/20210807102004
new file mode 100644
index 00000000000..e63485435f8
--- /dev/null
+++ b/db/schema_migrations/20210807102004
@@ -0,0 +1 @@
+a2454f9fca3b1cedf7a0f2288b69abe799fe1f9ff4e2fe26d2cadfdddea73a83
\ No newline at end of file
diff --git a/db/schema_migrations/20210816095826 b/db/schema_migrations/20210816095826
new file mode 100644
index 00000000000..079a83fae8f
--- /dev/null
+++ b/db/schema_migrations/20210816095826
@@ -0,0 +1 @@
+d1ad234656f49861d2ca7694d23116e930bba597fca32b1015db698cc23bdc1c
\ No newline at end of file
diff --git a/db/schema_migrations/20210818061156 b/db/schema_migrations/20210818061156
new file mode 100644
index 00000000000..fba5486b2a8
--- /dev/null
+++ b/db/schema_migrations/20210818061156
@@ -0,0 +1 @@
+23becdc9ad558882f4ce42e76391cdc2f760322a09c998082465fcb6d29dfeb5
\ No newline at end of file
diff --git a/db/schema_migrations/20210818115613 b/db/schema_migrations/20210818115613
new file mode 100644
index 00000000000..efe76d3a46a
--- /dev/null
+++ b/db/schema_migrations/20210818115613
@@ -0,0 +1 @@
+9c5114dac05e90c15567bb3274f20f03a82f9e4d73d5c72d89c26bc9d742cc35
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 70fa133912b..ef88af2a04f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12089,7 +12089,11 @@ CREATE TABLE dast_profile_schedules (
updated_at timestamp with time zone NOT NULL,
active boolean DEFAULT true NOT NULL,
cron text NOT NULL,
- CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255))
+ cadence jsonb DEFAULT '{}'::jsonb NOT NULL,
+ timezone text NOT NULL,
+ starts_at timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255)),
+ CONSTRAINT check_be4d1c3af1 CHECK ((char_length(timezone) <= 255))
);
COMMENT ON TABLE dast_profile_schedules IS '{"owner":"group::dynamic analysis","description":"Scheduling for scans using DAST Profiles"}';
@@ -23812,9 +23816,9 @@ CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_
CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at);
-CREATE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
+CREATE UNIQUE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
-CREATE UNIQUE INDEX index_dast_profile_schedules_on_project_id_and_dast_profile_id ON dast_profile_schedules USING btree (project_id, dast_profile_id);
+CREATE INDEX index_dast_profile_schedules_on_project_id ON dast_profile_schedules USING btree (project_id);
CREATE INDEX index_dast_profile_schedules_on_user_id ON dast_profile_schedules USING btree (user_id);
diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md
index a4b4fd9fd16..25b87366a30 100644
--- a/doc/ci/cloud_deployment/index.md
+++ b/doc/ci/cloud_deployment/index.md
@@ -95,7 +95,7 @@ GitLab provides a series of [CI templates that you can include in your project](
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
-GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `gitlab-ci.yml` file to simplify working with AWS:
+GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `.gitlab-ci.yml` file to simplify working with AWS:
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands.
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS.
diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md
index 4e34cc7ce38..1b34b520007 100644
--- a/doc/ci/environments/deployment_safety.md
+++ b/doc/ci/environments/deployment_safety.md
@@ -136,10 +136,10 @@ connect the CD project to your development projects by using [multi-project pipe
A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
deployment usually runs automatically after pushing a merge request. To prevent developers from
-changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can
+changing the `.gitlab-ci.yml`, you can define it in a different repository. The configuration can
reference a file in another project with a completely different set of permissions (similar to
[separating a project for deployments](#separate-project-for-deployments)).
-In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with
+In this scenario, the `.gitlab-ci.yml` is publicly accessible, but can only be edited by users with
appropriate permissions in the other project.
For more information, see [Custom CI/CD configuration path](../pipelines/settings.md#specify-a-custom-cicd-configuration-file).
diff --git a/doc/ci/environments/incremental_rollouts.md b/doc/ci/environments/incremental_rollouts.md
index 9c95ddc6fa0..bc52fd1f16c 100644
--- a/doc/ci/environments/incremental_rollouts.md
+++ b/doc/ci/environments/incremental_rollouts.md
@@ -135,5 +135,5 @@ to switch to a different deployment. Both deployments are running in parallel, a
can be switched to at any time.
An [example deployable application](https://gitlab.com/gl-release/blue-green-example)
-is available, with a [`gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
+is available, with a [`.gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
that demonstrates blue-green deployments.
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index db9fbec85ae..f0204180d8a 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -177,7 +177,7 @@ You can find the play button in the pipelines, environments, deployments, and jo
If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md)
associated with your project, you can configure these deployments from your
-`gitlab-ci.yml` file.
+`.gitlab-ci.yml` file.
NOTE:
Kubernetes configuration isn't supported for Kubernetes clusters that are
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index 3cd4ebdbdf1..dc2df68e918 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -251,7 +251,7 @@ To protect a group-level environment:
1. Make sure your environments have the correct
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in
- `gitlab-ci.yml`.
+ `.gitlab-ci.yml`.
1. Configure the group-level protected environments via the
[REST API](../../api/group_protected_environments.md).
diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md
index 7132d47d324..10a6d34b076 100644
--- a/doc/ci/pipeline_editor/index.md
+++ b/doc/ci/pipeline_editor/index.md
@@ -56,7 +56,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.12.
-To view a visualization of your `gitlab-ci.yml` configuration, in your project,
+To view a visualization of your `.gitlab-ci.yml` configuration, in your project,
go to **CI/CD > Editor**, and then select the **Visualize** tab. The
visualization shows all stages and jobs. Any [`needs`](../yaml/index.md#needs)
relationships are displayed as lines connecting jobs together, showing the
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index df9b20d1708..827a89fa99c 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -29,7 +29,7 @@ with your editor of choice.
### Verify syntax with CI Lint tool
The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
-file is correct. Paste in full `gitlab-ci.yml` files or individual jobs configuration,
+file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration,
to verify the basic syntax.
When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
@@ -49,7 +49,7 @@ and check if their values are what you expect.
## GitLab CI/CD documentation
-The [complete `gitlab-ci.yml` reference](yaml/index.md) contains a full list of
+The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of
every keyword you may need to use to configure your pipelines.
You can also look at a large number of pipeline configuration [examples](examples/index.md)
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 236911791f1..c307316472c 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -386,7 +386,7 @@ does not block triggered pipelines.
> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4.
Use `include` to include external YAML files in your CI/CD configuration.
-You can break down one long `gitlab-ci.yml` file into multiple files to increase readability,
+You can break down one long `.gitlab-ci.yml` file into multiple files to increase readability,
or reduce duplication of the same configuration in multiple places.
You can also store template files in a central repository and `include` them in projects.
@@ -4483,7 +4483,7 @@ deploy_review_job:
You can use only integers and strings for the variable's name and value.
-If you define a variable at the top level of the `gitlab-ci.yml` file, it is global,
+If you define a variable at the top level of the `.gitlab-ci.yml` file, it is global,
meaning it applies to all jobs. If you define a variable in a job, it's available
to that job only.
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index c96fe2c18c1..be46d61eb4c 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -98,6 +98,7 @@ EE: true
database records created during Cycle Analytics model spec."
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
+- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
- [Removing](feature_flags/#changelog) a feature flag, when the new code is retained.
## Writing good changelog entries
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 9a83b077f4e..e2f15e3362f 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -74,7 +74,7 @@ If your application utilizes Docker containers you have another option for deplo
After your Docker build job completes and your image is added to your container registry, you can use the image as a
[service](../../../ci/services/index.md).
-By using service definitions in your `gitlab-ci.yml`, you can scan services with the DAST analyzer.
+By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
```yaml
stages:
@@ -1307,9 +1307,9 @@ dast:
By default, DAST downloads all artifacts defined by previous jobs in the pipeline. If
your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created
in previous jobs, we recommend you don't download artifacts. To avoid downloading
-artifacts, add the following to your `gitlab-ci.yml` file:
+artifacts, add the following to your `.gitlab-ci.yml` file:
-```json
+```yaml
dast:
dependencies: []
```
diff --git a/doc/user/application_security/offline_deployments/index.md b/doc/user/application_security/offline_deployments/index.md
index 3bf9d85cd0b..cdf54070d69 100644
--- a/doc/user/application_security/offline_deployments/index.md
+++ b/doc/user/application_security/offline_deployments/index.md
@@ -111,7 +111,7 @@ example of such a transfer:
GitLab provides a [vendored template](../../../ci/yaml/index.md#includetemplate)
to ease this process.
-This template should be used in a new, empty project, with a `gitlab-ci.yml` file containing:
+This template should be used in a new, empty project, with a `.gitlab-ci.yml` file containing:
```yaml
include:
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index ec22f71157f..ca6b2ba686e 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -316,7 +316,7 @@ The optional `runtime` parameter can refer to one of the following runtime alias
| `openfaas/classic/python3` | OpenFaaS |
| `openfaas/classic/ruby` | OpenFaaS |
-After the `gitlab-ci.yml` template has been added and the `serverless.yml` file
+After the `.gitlab-ci.yml` template has been added and the `serverless.yml` file
has been created, pushing a commit to your project results in a CI pipeline
being executed which deploys each function as a Knative service. After the
deploy stage has finished, additional details for the function display
diff --git a/doc/user/project/merge_requests/test_coverage_visualization.md b/doc/user/project/merge_requests/test_coverage_visualization.md
index ce8bfa2d054..11360809ad7 100644
--- a/doc/user/project/merge_requests/test_coverage_visualization.md
+++ b/doc/user/project/merge_requests/test_coverage_visualization.md
@@ -129,7 +129,7 @@ The `source` is ignored if the path does not follow this pattern. The parser ass
### JavaScript example
-The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
+The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
JavaScript testing and [nyc](https://github.com/istanbuljs/nyc) coverage-tooling to
generate the coverage artifact:
@@ -147,7 +147,7 @@ test:
#### Maven example
-The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
+The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
@@ -185,7 +185,7 @@ coverage-jdk11:
#### Gradle example
-The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
+The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
@@ -223,7 +223,7 @@ coverage-jdk11:
### Python example
-The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
+The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
The information isn't displayed without the conversion.
This example assumes that the code for your package is in `src/` and your tests are in `tests.py`:
@@ -243,7 +243,7 @@ run tests:
### C/C++ example
-The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
+The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
`gcc` or `g++` as the compiler uses [`gcovr`](https://gcovr.com/en/stable/) to generate the coverage
output file in Cobertura XML format.
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 5a688fbb485..385a4fafa7d 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -46,7 +46,7 @@ To create a GitLab Pages website:
| Document | Description |
| -------- | ----------- |
-| [Create a `gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
+| [Create a `.gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
| [Use a `.gitlab-ci.yml` template](getting_started/pages_ci_cd_template.md) | Add a Pages site to an existing project. Use a pre-populated CI template file. |
| [Fork a sample project](getting_started/pages_forked_sample_project.md) | Create a new project with Pages already configured by forking a sample project. |
| [Use a project template](getting_started/pages_new_project_template.md) | Create a new project with Pages already configured by using a template. |
diff --git a/doc/user/project/releases/img/deploy_freeze_v13_10.png b/doc/user/project/releases/img/deploy_freeze_v13_10.png
deleted file mode 100644
index 5c4b2d983dd..00000000000
Binary files a/doc/user/project/releases/img/deploy_freeze_v13_10.png and /dev/null differ
diff --git a/doc/user/project/releases/img/deploy_freeze_v14_3.png b/doc/user/project/releases/img/deploy_freeze_v14_3.png
new file mode 100644
index 00000000000..396c8b5cbae
Binary files /dev/null and b/doc/user/project/releases/img/deploy_freeze_v14_3.png differ
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index 76b300bdd57..e6033c844be 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -186,7 +186,8 @@ To subscribe to notifications for releases:
## Prevent unintentional releases by setting a deploy freeze
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
+> - The ability to delete freeze periods through the UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212451) in GitLab 14.3.
Prevent unintended production releases during a period of time you specify by
setting a [*deploy freeze* period](../../../ci/environments/deployment_safety.md).
@@ -199,7 +200,7 @@ If the job that's executing is within a freeze period, GitLab CI/CD creates an e
variable named `$CI_DEPLOY_FREEZE`.
To prevent the deployment job from executing, create a `rules` entry in your
-`gitlab-ci.yml`, for example:
+`.gitlab-ci.yml`, for example:
```yaml
deploy_to_production:
@@ -219,11 +220,8 @@ To set a deploy freeze window in the UI, complete these steps:
1. Click **Add deploy freeze** to open the deploy freeze modal.
1. Enter the start time, end time, and timezone of the desired deploy freeze period.
1. Click **Add deploy freeze** in the modal.
-1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**).
- ![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v13_10.png)
-
-WARNING:
-To delete a deploy freeze, use the [Freeze Periods API](../../../api/freeze_periods.md).
+1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**) and remove it by selecting the delete button (**{remove}**).
+ ![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v14_3.png)
If a project contains multiple freeze periods, all periods apply. If they overlap, the freeze covers the
complete overlapping period.
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index bc03658aab8..7334a112ccf 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -6,8 +6,40 @@ module Gitlab
VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
VALID_SYNTAX_SAMPLE_CRON = '* * * * *'
- def self.parse_natural(expression, cron_timezone = 'UTC')
- new(Fugit::Nat.parse(expression)&.original, cron_timezone)
+ class << self
+ def parse_natural(expression, cron_timezone = 'UTC')
+ new(Fugit::Nat.parse(expression)&.original, cron_timezone)
+ end
+
+ # This method generates compatible expressions that can be
+ # parsed by Fugit::Nat.parse to generate a cron line.
+ # It takes start date of the cron and cadence in the following format:
+ # cadence = {
+ # unit: 'day/week/month/year'
+ # duration: 1
+ # }
+ def parse_natural_with_timestamp(starts_at, cadence)
+ case cadence[:unit]
+ when 'day' # Currently supports only 'every 1 day'.
+ "#{starts_at.min} #{starts_at.hour} * * *"
+ when 'week' # Currently supports only 'every 1 week'.
+ "#{starts_at.min} #{starts_at.hour} * * #{starts_at.wday}"
+ when 'month'
+ unless [1, 3, 6, 12].include?(cadence[:duration])
+ raise NotImplementedError, "The cadence #{cadence} is not supported"
+ end
+
+ "#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{fall_in_months(cadence[:duration], starts_at)} *"
+ when 'year' # Currently supports only 'every 1 year'.
+ "#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{starts_at.month} *"
+ else
+ raise NotImplementedError, "The cadence unit #{cadence[:unit]} is not implemented"
+ end
+ end
+
+ def fall_in_months(offset, start_date)
+ (1..(12 / offset)).map { |i| start_date.next_month(offset * i).month }.join(',')
+ end
end
def initialize(cron, cron_timezone = 'UTC')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 51b9cdbd1c6..4039f5a2f37 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11068,6 +11068,18 @@ msgstr ""
msgid "DeployFreeze|Add deploy freeze"
msgstr ""
+msgid "DeployFreeze|Delete"
+msgstr ""
+
+msgid "DeployFreeze|Delete deploy freeze?"
+msgstr ""
+
+msgid "DeployFreeze|Delete freeze period"
+msgstr ""
+
+msgid "DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?"
+msgstr ""
+
msgid "DeployFreeze|Edit"
msgstr ""
@@ -13262,6 +13274,9 @@ msgstr ""
msgid "Error: Unable to create deploy freeze"
msgstr ""
+msgid "Error: Unable to delete deploy freeze"
+msgstr ""
+
msgid "Error: Unable to find AWS role for current user"
msgstr ""
@@ -28991,6 +29006,9 @@ msgstr ""
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor."
msgstr ""
+msgid "Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?"
+msgstr ""
+
msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner."
msgstr ""
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 65a563fac7c..774971e992d 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -311,23 +311,42 @@ RSpec.describe Projects::PipelinesController do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- def create_build_with_artifacts(stage, stage_idx, name)
- create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ def create_build_with_artifacts(stage, stage_idx, name, status)
+ create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ end
+
+ def create_bridge(stage, stage_idx, name, status)
+ create(:ci_bridge, status, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end
before do
- create_build_with_artifacts('build', 0, 'job1')
- create_build_with_artifacts('build', 0, 'job2')
+ create_build_with_artifacts('build', 0, 'job1', :failed)
+ create_build_with_artifacts('build', 0, 'job2', :running)
+ create_build_with_artifacts('build', 0, 'job3', :pending)
+ create_bridge('deploy', 1, 'deploy-a', :failed)
+ create_bridge('deploy', 1, 'deploy-b', :created)
end
- it 'avoids N+1 database queries', :request_store do
- control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count
+ it 'avoids N+1 database queries', :request_store, :use_sql_query_cache do
+ # warm up
+ get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
- create_build_with_artifacts('build', 0, 'job3')
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get_pipeline_html
+ expect(response).to have_gitlab_http_status(:ok)
+ end
- expect { get_pipeline_html }.not_to exceed_query_limit(control_count)
- expect(response).to have_gitlab_http_status(:ok)
+ create_build_with_artifacts('build', 0, 'job4', :failed)
+ create_build_with_artifacts('build', 0, 'job5', :running)
+ create_build_with_artifacts('build', 0, 'job6', :pending)
+ create_bridge('deploy', 1, 'deploy-c', :failed)
+ create_bridge('deploy', 1, 'deploy-d', :created)
+
+ expect do
+ get_pipeline_html
+ expect(response).to have_gitlab_http_status(:ok)
+ end.not_to exceed_all_query_limit(control)
end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index a5a0f16f2b1..5edd60ebc79 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -283,6 +283,26 @@ RSpec.describe RegistrationsController do
end
end
+ context 'when the registration fails' do
+ let_it_be(:member) { create(:project_member, :invited) }
+ let_it_be(:missing_user_params) do
+ { username: '', email: member.invite_email, password: 'Any_password' }
+ end
+
+ let_it_be(:user_params) { { user: missing_user_params } }
+
+ let(:session_params) { { invite_email: member.invite_email } }
+
+ subject { post(:create, params: user_params, session: session_params) }
+
+ it 'does not delete the invitation or register the new user' do
+ subject
+
+ expect(member.invite_token).not_to be_nil
+ expect(controller.current_user).to be_nil
+ end
+ end
+
context 'when soft email confirmation is enabled' do
before do
stub_feature_flags(soft_email_confirmation: true)
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 583daba37f1..87fb8955dcc 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -189,6 +189,16 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end
context 'email confirmation enabled' do
+ context 'when user is not valid in sign up form' do
+ let(:new_user) { build_stubbed(:user, first_name: '', last_name: '') }
+
+ it 'fails sign up and redirects back to sign up', :aggregate_failures do
+ expect { fill_in_sign_up_form(new_user) }.not_to change { User.count }
+ expect(page).to have_content('prohibited this user from being saved')
+ expect(current_path).to eq(user_registration_path)
+ end
+ end
+
context 'with invite email acceptance', :snowplow do
it 'tracks the accepted invite' do
fill_in_sign_up_form(new_user)
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index 168ddcfeacc..403d0dce3fc 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -1,3 +1,4 @@
+import { GlModal } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
@@ -29,6 +30,8 @@ describe('Deploy freeze table', () => {
const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]');
const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
+ const findDeleteDeployFreezeButton = () => wrapper.find('[data-testid="delete-deploy-freeze"]');
+ const findDeleteDeployFreezeModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
createComponent();
@@ -73,6 +76,29 @@ describe('Deploy freeze table', () => {
store.state.freezePeriods[0],
);
});
+
+ it('displays delete deploy freeze button', () => {
+ expect(findDeleteDeployFreezeButton().exists()).toBe(true);
+ });
+
+ it('confirms a user wants to delete a deploy freeze', async () => {
+ const [{ freezeStart, freezeEnd, cronTimezone }] = store.state.freezePeriods;
+ await findDeleteDeployFreezeButton().trigger('click');
+ const modal = findDeleteDeployFreezeModal();
+ expect(modal.text()).toContain(
+ `Deploy freeze from ${freezeStart} to ${freezeEnd} in ${cronTimezone.formattedTimezone} will be removed.`,
+ );
+ });
+
+ it('deletes the freeze period on confirmation', async () => {
+ await findDeleteDeployFreezeButton().trigger('click');
+ const modal = findDeleteDeployFreezeModal();
+ modal.vm.$emit('primary');
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'deleteFreezePeriod',
+ store.state.freezePeriods[0],
+ );
+ });
});
});
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 6bc9c4d374c..ac606f1b182 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -12,6 +12,7 @@ jest.mock('~/api.js');
jest.mock('~/flash.js');
describe('deploy freeze store actions', () => {
+ const freezePeriodFixture = freezePeriodsFixture[0];
let mock;
let state;
@@ -24,6 +25,7 @@ describe('deploy freeze store actions', () => {
Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
Api.createFreezePeriod.mockResolvedValue();
Api.updateFreezePeriod.mockResolvedValue();
+ Api.deleteFreezePeriod.mockResolvedValue();
});
afterEach(() => {
@@ -195,4 +197,46 @@ describe('deploy freeze store actions', () => {
);
});
});
+
+ describe('deleteFreezePeriod', () => {
+ it('dispatch correct actions on deleting a freeze period', () => {
+ testAction(
+ actions.deleteFreezePeriod,
+ freezePeriodFixture,
+ state,
+ [
+ { type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
+ { type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
+ ],
+ [],
+ () =>
+ expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
+ state.projectId,
+ freezePeriodFixture.id,
+ ),
+ );
+ });
+
+ it('should show flash error and set error in state on delete failure', () => {
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation();
+ const error = new Error();
+ Api.deleteFreezePeriod.mockRejectedValue(error);
+
+ testAction(
+ actions.deleteFreezePeriod,
+ freezePeriodFixture,
+ state,
+ [
+ { type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
+ { type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
+ ],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+
+ expect(errorSpy).toHaveBeenCalledWith('[gitlab] Unable to delete deploy freeze:', error);
+ },
+ );
+ });
+ });
});
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
index f8683489340..878a755088c 100644
--- a/spec/frontend/deploy_freeze/store/mutations_spec.js
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -28,9 +28,9 @@ describe('Deploy freeze mutations', () => {
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
it('should set freeze periods and format timezones from identifiers to names', () => {
const timezoneNames = {
- 'Europe/Berlin': 'Berlin',
- 'Etc/UTC': 'UTC',
- 'America/New_York': 'Eastern Time (US & Canada)',
+ 'Europe/Berlin': '[UTC 2] Berlin',
+ 'Etc/UTC': '[UTC 0] UTC',
+ 'America/New_York': '[UTC -4] Eastern Time (US & Canada)',
};
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index a568a7d5396..b930259149f 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -31,7 +31,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
- canReadEnvironment: true,
tableData,
},
});
@@ -135,7 +134,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutDeployable,
- canReadEnvironment: true,
tableData,
},
});
@@ -161,7 +159,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutUpcomingDeployment,
- canReadEnvironment: true,
tableData,
},
});
@@ -177,7 +174,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -205,7 +201,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: futureDate,
},
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -241,7 +236,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: pastDate,
},
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -360,7 +354,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: folder,
- canReadEnvironment: true,
tableData,
},
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index 71426ee5170..1851163ac68 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -28,7 +28,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [folder],
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -50,7 +49,6 @@ describe('Environment table', () => {
await factory({
propsData: {
environments: [mockItem],
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -78,7 +76,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -114,7 +111,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -151,7 +147,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [mockItem],
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -179,7 +174,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -230,7 +224,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -296,7 +289,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -335,7 +327,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -364,7 +355,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -415,7 +405,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index dc176001943..afe2e4cdc63 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -20,7 +20,6 @@ describe('Environment', () => {
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
- canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
userCalloutsPath: '/callouts',
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 6334060c736..305e7385b43 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -44,7 +44,6 @@ describe('Environments detail header component', () => {
TimeAgo,
},
propsData: {
- canReadEnvironment: false,
canAdminEnvironment: false,
canUpdateEnvironment: false,
canStopEnvironment: false,
@@ -60,7 +59,7 @@ describe('Environments detail header component', () => {
describe('default state with minimal access', () => {
beforeEach(() => {
- createWrapper({ props: { environment: createEnvironment() } });
+ createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
});
it('displays the environment name', () => {
@@ -164,7 +163,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment({ hasTerminals: true, externalUrl }),
- canReadEnvironment: true,
},
});
});
@@ -178,8 +176,7 @@ describe('Environments detail header component', () => {
beforeEach(() => {
createWrapper({
props: {
- environment: createEnvironment(),
- canReadEnvironment: true,
+ environment: createEnvironment({ metricsUrl: 'my metrics url' }),
metricsPath,
},
});
@@ -195,7 +192,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment(),
- canReadEnvironment: true,
canAdminEnvironment: true,
canStopEnvironment: true,
canUpdateEnvironment: true,
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index e4661d27872..72a7449f24e 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -11,7 +11,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
- canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index d02ed8688c6..9eb57b2682f 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -14,7 +14,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
- canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
diff --git a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
new file mode 100644
index 00000000000..6735dddf51e
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
@@ -0,0 +1,25 @@
+import { shallowMount } from '@vue/test-utils';
+import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
+
+describe('Image Viewer', () => {
+ let wrapper;
+
+ const propsData = {
+ url: 'some/image.png',
+ alt: 'image.png',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(ImageViewer, { propsData });
+ };
+
+ const findImage = () => wrapper.find('[data-testid="image"]');
+
+ it('renders a Source Editor component', () => {
+ createComponent();
+
+ expect(findImage().exists()).toBe(true);
+ expect(findImage().attributes('src')).toBe(propsData.url);
+ expect(findImage().attributes('alt')).toBe(propsData.alt);
+ });
+});
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 6908bcbd283..9fa3bfc1f9a 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => {
it('highlights lines with search term occurrence', () => {
setHighlightClass(searchKeyword);
- expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
+ expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
});
});
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index 6ffc8b045e9..e63c0d60398 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -20,18 +20,20 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let!(:issue1) { create(:issue, project: project, labels: [label], relative_position: 10) }
let!(:issue2) { create(:issue, project: project, labels: [label, label2], relative_position: 12) }
let!(:issue3) { create(:issue, project: project, labels: [label, label3], relative_position: 10) }
+ let!(:issue4) { create(:issue, project: project, labels: [label], relative_position: nil) }
- it 'returns the issues in the correct order' do
+ it 'returns issues in the correct order with non-nil relative positions', :aggregate_failures do
# by relative_position and then ID
- issues = resolve_board_list_issues
+ result = resolve_board_list_issues
- expect(issues.map(&:id)).to eq [issue3.id, issue1.id, issue2.id]
+ expect(result.map(&:id)).to eq [issue3.id, issue1.id, issue2.id, issue4.id]
+ expect(result.map(&:relative_position)).not_to include(nil)
end
it 'finds only issues matching filters' do
result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } })
- expect(result).to match_array([issue1, issue3])
+ expect(result).to match_array([issue1, issue3, issue4])
end
it 'finds only issues matching search param' do
@@ -49,7 +51,7 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
it 'accepts assignee wildcard id NONE' do
result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'NONE' } })
- expect(result).to match_array([issue1, issue2, issue3])
+ expect(result).to match_array([issue1, issue2, issue3, issue4])
end
it 'accepts assignee wildcard id ANY' do
@@ -89,6 +91,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
end
def resolve_board_list_issues(args: {}, current_user: user)
- resolve(described_class, obj: list, args: args, ctx: { current_user: current_user })
+ resolve(described_class, obj: list, args: args, ctx: { current_user: current_user }).items
end
end
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
index 0eecae32cc1..49937a3b53a 100644
--- a/spec/helpers/environment_helper_spec.rb
+++ b/spec/helpers/environment_helper_spec.rb
@@ -43,7 +43,6 @@ RSpec.describe EnvironmentHelper do
external_url: environment.external_url,
can_update_environment: true,
can_destroy_environment: true,
- can_read_environment: true,
can_stop_environment: true,
can_admin_environment: true,
environment_metrics_path: environment_metrics_path(environment),
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 15293429354..4017accb462 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -297,4 +297,65 @@ RSpec.describe Gitlab::Ci::CronParser do
it { is_expected.to eq(true) }
end
end
+
+ describe '.parse_natural', :aggregate_failures do
+ let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'day', duration: 1 }) }
+ let(:time) { Time.parse('Mon, 30 Aug 2021 06:29:44.067132000 UTC +00:00') }
+ let(:hours) { Fugit::Cron.parse(cron_line).hours }
+ let(:minutes) { Fugit::Cron.parse(cron_line).minutes }
+ let(:weekdays) { Fugit::Cron.parse(cron_line).weekdays.first }
+ let(:months) { Fugit::Cron.parse(cron_line).months }
+
+ context 'when repeat cycle is day' do
+ it 'generates daily cron expression', :aggregate_failures do
+ expect(hours).to include time.hour
+ expect(minutes).to include time.min
+ end
+ end
+
+ context 'when repeat cycle is week' do
+ let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'week', duration: 1 }) }
+
+ it 'generates weekly cron expression', :aggregate_failures do
+ expect(hours).to include time.hour
+ expect(minutes).to include time.min
+ expect(weekdays).to include time.wday
+ end
+ end
+
+ context 'when repeat cycle is month' do
+ let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 3 }) }
+
+ it 'generates monthly cron expression', :aggregate_failures do
+ expect(minutes).to include time.min
+ expect(months).to include time.month
+ end
+
+ context 'when an unsupported duration is specified' do
+ subject { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 7 }) }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(NotImplementedError, 'The cadence {:unit=>"month", :duration=>7} is not supported')
+ end
+ end
+ end
+
+ context 'when repeat cycle is year' do
+ let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'year', duration: 1 }) }
+
+ it 'generates yearly cron expression', :aggregate_failures do
+ expect(hours).to include time.hour
+ expect(minutes).to include time.min
+ expect(months).to include time.month
+ end
+ end
+
+ context 'when the repeat cycle is not implemented' do
+ subject { described_class.parse_natural_with_timestamp(time, { unit: 'quarterly', duration: 1 }) }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(NotImplementedError, 'The cadence unit quarterly is not implemented')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index cf340b47b68..ee939823129 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -224,6 +224,7 @@ ci_pipelines:
- builds
- bridges
- processables
+- generic_commit_statuses
- trigger_requests
- variables
- auto_canceled_by
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 067b3c25645..3f7f69ff34e 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -645,6 +645,16 @@ RSpec.describe Member do
expect(user.authorized_projects.reload).to include(project)
end
+
+ it 'does not accept the invite if saving a new user fails' do
+ invalid_user = User.new(first_name: '', last_name: '')
+
+ member.accept_invite! invalid_user
+
+ expect(member.invite_accepted_at).to be_nil
+ expect(member.invite_token).not_to be_nil
+ expect_any_instance_of(Member).not_to receive(:after_accept_invite)
+ end
end
describe "#decline_invite!" do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index e26355b1eb4..a2b818576a6 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1576,6 +1576,14 @@ RSpec.describe Note do
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}")
end
+ context 'when note has no author' do
+ let(:note) { build(:note, author: nil) }
+
+ it 'returns cache key only' do
+ expect(note.post_processed_cache_key).to eq("#{note.cache_key}:")
+ end
+ end
+
context 'when note has redacted_note_html' do
let(:redacted_note_html) { 'redacted note html' }
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index 3628171fcc1..008241b8055 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -48,13 +48,18 @@ RSpec.describe 'get board lists' do
issues_data.map { |i| i['title'] }
end
+ def issue_relative_positions
+ issues_data.map { |i| i['relativePosition'] }
+ end
+
shared_examples 'group and project board list issues query' do
let!(:board) { create(:board, resource_parent: board_parent) }
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) }
let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) }
- let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
- let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
+ let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) }
+ let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
+ let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
context 'when the user does not have access to the board' do
it 'returns nil' do
@@ -69,10 +74,11 @@ RSpec.describe 'get board lists' do
board_parent.add_reporter(user)
end
- it 'can access the issues' do
+ it 'can access the issues', :aggregate_failures do
post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user)
- expect(issue_titles).to eq([issue2.title, issue1.title])
+ expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title])
+ expect(issue_relative_positions).not_to include(nil)
end
end
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index cb6755640a9..d46ef313563 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -311,6 +311,10 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ # create extra statuses
+ create(:generic_commit_status, :pending, name: 'generic-build-a', pipeline: pipeline, stage_idx: 0, stage: 'build')
+ create(:ci_bridge, :failed, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
+
# warm up
post_graphql(query, current_user: current_user)
@@ -318,9 +322,11 @@ RSpec.describe 'getting pipeline information nested in a project' do
post_graphql(query, current_user: current_user)
end
- create(:ci_build, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
- create(:ci_build, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
- create(:ci_build, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
+ create(:generic_commit_status, :pending, name: 'generic-build-b', pipeline: pipeline, stage_idx: 0, stage: 'build')
+ create(:ci_build, :failed, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
+ create(:ci_build, :running, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
+ create(:ci_build, :pending, name: 'deploy-b', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
+ create(:ci_bridge, :failed, name: 'deploy-c', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
expect do
post_graphql(query, current_user: current_user)
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index bbdc178b234..d1f854f72bc 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -139,4 +139,51 @@ RSpec.describe Boards::Issues::ListService do
end
# rubocop: enable RSpec/MultipleMemoizedHelpers
end
+
+ describe '.initialize_relative_positions' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:backlog) { create(:backlog_list, board: board) }
+
+ let(:issue) { create(:issue, project: project, relative_position: nil) }
+
+ context "when 'Gitlab::Database::read_write?' is true" do
+ before do
+ allow(Gitlab::Database).to receive(:read_write?).and_return(true)
+ end
+
+ context 'user cannot move issues' do
+ it 'does not initialize the relative positions of issues' do
+ described_class.initialize_relative_positions(board, user, [issue])
+
+ expect(issue.relative_position).to eq nil
+ end
+ end
+
+ context 'user can move issues' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'initializes the relative positions of issues' do
+ described_class.initialize_relative_positions(board, user, [issue])
+
+ expect(issue.relative_position).not_to eq nil
+ end
+ end
+ end
+
+ context "when 'Gitlab::Database::read_write?' is false" do
+ before do
+ allow(Gitlab::Database).to receive(:read_write?).and_return(false)
+ end
+
+ it 'does not initialize the relative positions of issues' do
+ described_class.initialize_relative_positions(board, user, [issue])
+
+ expect(issue.relative_position).to eq nil
+ end
+ end
+ end
end