Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-17 12:10:08 +00:00
parent b9b58dba70
commit 8060e5c609
132 changed files with 2365 additions and 1458 deletions

View File

@ -236,6 +236,21 @@ db:check-migrations-single-db:
- .single-db
- .rails:rules:single-db
db:post_deployment_migrations_validator:
extends:
- .db-job-base
- .rails:rules:ee-and-foss-mr-with-migration
script:
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME:$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --depth 20
- scripts/post_deployment_migrations_validator
allow_failure: true
db:post_deployment_migrations_validator-single-db:
extends:
- db:post_deployment_migrations_validator
- .single-db
- .rails:rules:single-db
db:migrate-non-superuser:
extends:
- .db-job-base

View File

@ -241,7 +241,7 @@ gem 'ruby-progressbar', '~> 1.10'
gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
gem 're2', '~> 1.4.0'
gem 're2', '~> 1.5.0'
# Misc
@ -365,7 +365,7 @@ gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client'
gem 'warning', '~> 1.3.0'
group :development do
gem 'lefthook', '~> 1.1.2', require: false
gem 'lefthook', '~> 1.1.3', require: false
gem 'rubocop'
gem 'solargraph', '~> 0.47.2', require: false

View File

@ -302,7 +302,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.1.2","platform":"ruby","checksum":"fdbe2a62faf6ae2a9f9be64e105ca8f06c527c5ba9a3edb2cdcf024025227897"},
{"name":"lefthook","version":"1.1.3","platform":"ruby","checksum":"3f8337b2176f49e6d4ab8f0f4494c8d1be0548d79bca898fbf2184d717092b75"},
{"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"},
@ -455,7 +455,7 @@
{"name":"rbtree","version":"0.4.4","platform":"ruby","checksum":"c1277a502a96fe8fd8656cb619db1ac87145df809ea4db35f7242b50bb161d5c"},
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
{"name":"rdoc","version":"6.3.2","platform":"ruby","checksum":"def4a720235c27d56c176ae73555e647eb04ea58a8bbaa927f8f9f79de7805a6"},
{"name":"re2","version":"1.4.0","platform":"ruby","checksum":"5c07d2351be1159530e2b815aae499b6524942e79bf21d560fcff5b2fa20ea8f"},
{"name":"re2","version":"1.5.0","platform":"ruby","checksum":"35fe8b408de9f1ef609b1e54e01ea1e55413ca3e9daf1e4b20756d9a02f630cc"},
{"name":"recaptcha","version":"4.13.1","platform":"ruby","checksum":"dc6c2cb78afa87034358b7ba1c6f7175972b5709fdf7500e2550623e119e3788"},
{"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
{"name":"redcarpet","version":"3.5.1","platform":"ruby","checksum":"717f64cb6ec11c8d9ec9b521ed26ca2eeda68b4fe1fc3388a641176dbd47732f"},

View File

@ -801,7 +801,7 @@ GEM
rest-client (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
lefthook (1.1.2)
lefthook (1.1.3)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (2.0.0)
@ -1136,7 +1136,7 @@ GEM
rbtree (0.4.4)
rchardet (1.8.0)
rdoc (6.3.2)
re2 (1.4.0)
re2 (1.5.0)
recaptcha (4.13.1)
json
recursive-open-struct (1.1.3)
@ -1680,7 +1680,7 @@ DEPENDENCIES
knapsack (~> 1.21.1)
kramdown (~> 2.3.1)
kubeclient (~> 4.9.3)
lefthook (~> 1.1.2)
lefthook (~> 1.1.3)
letter_opener_web (~> 2.0.0)
license_finder (~> 7.0)
licensee (~> 9.15)
@ -1751,7 +1751,7 @@ DEPENDENCIES
rainbow (~> 3.0)
rbtrace (~> 0.4)
rdoc (~> 6.3.2)
re2 (~> 1.4.0)
re2 (~> 1.5.0)
recaptcha (~> 4.11)
redis (~> 4.7.0)
redis-actionpack (~> 5.3.0)
@ -1834,4 +1834,4 @@ DEPENDENCIES
yajl-ruby (~> 1.4.3)
BUNDLED WITH
2.3.22
2.3.23

View File

@ -57,8 +57,8 @@ export default {
/** @type {HTMLFormElement} */
this.form = document.querySelector(FORM_SELECTOR);
/** @type {HTMLInputElement} */
this.submitButton = this.form.querySelector('input[type=submit]');
/** @type {HTMLButtonElement} */
this.submitButton = this.form.querySelector('[type=submit]');
},
methods: {
beforeDisplayResults() {
@ -75,6 +75,7 @@ export default {
this.errors = errors;
this.submitButton.classList.remove('disabled');
this.submitButton.removeAttribute('disabled');
},
onSuccess(event) {
this.beforeDisplayResults();
@ -84,7 +85,7 @@ export default {
this.infoAlert = createAlert({ message: this.alertInfoMessage, variant: VARIANT_INFO });
// Selectively reset all input fields except for the date picker and submit.
// Selectively reset all input fields except for the date picker.
// The form token creation is not controlled by Vue.
this.form.querySelectorAll('input[type=text]:not([id$=expires_at])').forEach((el) => {
el.value = '';

View File

@ -149,11 +149,6 @@ export default {
isSearchFiltered() {
return isSearchFiltered(this.search);
},
shouldRenderAllAvailableToggle() {
// Feature flag for `runners_finder_all_available`
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/374525
return this.glFeatures?.runnersFinderAllAvailable;
},
},
watch: {
search: {
@ -235,7 +230,6 @@ export default {
class="gl-flex-grow-1 gl-align-self-stretch"
/>
<runner-membership-toggle
v-if="shouldRenderAllAvailableToggle"
v-model="search.membership"
class="gl-align-self-end gl-md-align-self-center"
/>

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
import createFlash from './flash';
import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
export default class TaskList {
@ -23,7 +23,7 @@ export default class TaskList {
errorMessages = e.response.data.errors.join(' ');
}
return createFlash({
return createAlert({
message: errorMessages || __('Update failed'),
});
};

View File

@ -9,7 +9,7 @@ import {
GlSprintf,
GlToggle,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
@ -63,7 +63,7 @@ export default {
return data.project.ciCdSettings.jobTokenScopeEnabled;
},
error() {
createFlash({ message: this.$options.i18n.scopeFetchError });
createAlert({ message: this.$options.i18n.scopeFetchError });
},
},
projects: {
@ -77,7 +77,7 @@ export default {
return data.project?.ciJobTokenScope?.projects?.nodes ?? [];
},
error() {
createFlash({ message: this.$options.i18n.projectsFetchError });
createAlert({ message: this.$options.i18n.projectsFetchError });
},
},
},
@ -117,7 +117,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createFlash({ message: error });
createAlert({ message: error });
}
},
async addProject() {
@ -140,7 +140,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createFlash({ message: error });
createAlert({ message: error });
} finally {
this.clearTargetProjectPath();
this.getProjects();
@ -166,7 +166,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createFlash({ message: error });
createAlert({ message: error });
} finally {
this.getProjects();
}

View File

@ -1,6 +1,6 @@
<script>
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
@ -139,7 +139,7 @@ export default {
this.fetchingApprovals = false;
})
.catch(() =>
createFlash({
createAlert({
message: FETCH_ERROR,
}),
);
@ -154,7 +154,7 @@ export default {
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
createFlash({
createAlert({
message: APPROVE_ERROR,
}),
);
@ -167,7 +167,7 @@ export default {
this.hasApprovalAuthError = true;
return;
}
createFlash({
createAlert({
message: APPROVE_ERROR,
});
},
@ -177,7 +177,7 @@ export default {
this.updateApproval(
() => this.service.unapproveMergeRequest(),
() =>
createFlash({
createAlert({
message: UNAPPROVE_ERROR,
}),
);

View File

@ -1,5 +1,5 @@
<script>
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@ -130,7 +130,7 @@ export default {
}
})
.catch(() => {
createFlash({
createAlert({
message: errorMessage,
});
})

View File

@ -2,7 +2,7 @@
import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
@ -113,7 +113,7 @@ export default {
})
.catch(() => {
this.isCancellingAutoMerge = false;
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@ -141,7 +141,7 @@ export default {
})
.catch(() => {
this.isRemovingSourceBranch = false;
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
});

View File

@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import api from '~/api';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
@ -131,7 +131,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
});

View File

@ -1,6 +1,6 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
@ -111,7 +111,7 @@ export default {
if (error.response && error.response.data && error.response.data.merge_error) {
this.rebasingError = error.response.data.merge_error;
} else {
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
}
@ -142,7 +142,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
stopPolling();

View File

@ -14,7 +14,7 @@ import {
import { isEmpty } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
import { __, s__, n__ } from '~/locale';
@ -444,7 +444,7 @@ export default {
.catch(() => {
this.isMakingRequest = false;
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@ -488,7 +488,7 @@ export default {
}
})
.catch(() => {
createFlash({
createAlert({
message: __('Something went wrong while deleting the source branch. Please try again.'),
});
});

View File

@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
import $ from 'jquery';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import MergeRequest from '~/merge_request';
@ -77,7 +77,7 @@ export default {
},
) {
if (errors?.length) {
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
@ -130,7 +130,7 @@ export default {
},
)
.catch(() =>
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
}),
)
@ -152,7 +152,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
});
});

View File

@ -6,7 +6,7 @@ import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/ap
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import { stateToComponentMap as classState } from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
@ -267,7 +267,7 @@ export default {
this.initWidget(data);
})
.catch(() =>
createFlash({
createAlert({
message: __('Unable to load the merge request widget. Try reloading the page.'),
}),
);
@ -359,7 +359,7 @@ export default {
}
})
.catch(() =>
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
}),
);
@ -418,7 +418,7 @@ export default {
.catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
createFlash({
createAlert({
message: __(
'Something went wrong while fetching the environments for this merge request. Please try again.',
),
@ -438,7 +438,7 @@ export default {
}
})
.catch(() =>
createFlash({
createAlert({
message: __('Something went wrong. Please try again.'),
}),
);

View File

@ -22,11 +22,10 @@ module AccessTokensActions
if token_response.success?
@resource_access_token = token_response.payload[:access_token]
PersonalAccessToken.redis_store!(key_identity, @resource_access_token.token)
redirect_to resource_access_tokens_path, notice: _("Your new access token has been created.")
render json: { new_token: @resource_access_token.token,
active_access_tokens: active_resource_access_tokens }, status: :ok
else
redirect_to resource_access_tokens_path, alert: _("Failed to create new access token: %{token_response_message}") % { token_response_message: token_response.message }
render json: { errors: token_response.errors }, status: :unprocessable_entity
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@ -63,12 +62,15 @@ module AccessTokensActions
resource.members.load
@scopes = Gitlab::Auth.resource_bot_scopes
@active_resource_access_tokens = finder(state: 'active').execute.preload_users
@inactive_resource_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
@new_resource_access_token = PersonalAccessToken.redis_getdel(key_identity)
@active_resource_access_tokens = active_resource_access_tokens
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def active_resource_access_tokens
tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute.preload_users
represent(tokens)
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end

View File

@ -5,10 +5,6 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
before_action do
push_frontend_feature_flag(:runners_finder_all_available, @group)
end
before_action only: [:show] do
push_frontend_feature_flag(:enforce_runner_token_expires_at)
end
@ -41,8 +37,7 @@ class Groups::RunnersController < Groups::ApplicationController
private
def runner
group_params = { group: @group }
group_params[:membership] = :all_available if Feature.enabled?(:runners_finder_all_available, @group)
group_params = { group: @group, membership: :all_available }
@runner ||= Ci::RunnersFinder.new(current_user: current_user, params: group_params).execute
.except(:limit, :offset)

View File

@ -13,6 +13,12 @@ module Groups
def resource_access_tokens_path
group_settings_access_tokens_path
end
private
def represent(tokens)
::GroupAccessTokenSerializer.new.represent(tokens, group: resource)
end
end
end
end

View File

@ -81,9 +81,9 @@ class GroupsController < Groups::ApplicationController
successful_creation_hooks
notice = if @group.chat_team.present?
"Group '#{@group.name}' and its Mattermost team were successfully created."
format(_("Group %{group_name} and its Mattermost team were successfully created."), group_name: @group.name)
else
"Group '#{@group.name}' was successfully created."
format(_("Group %{group_name} was successfully created."), group_name: @group.name)
end
redirect_to @group, notice: notice

View File

@ -13,6 +13,12 @@ module Projects
def resource_access_tokens_path
namespace_project_settings_access_tokens_path
end
private
def represent(tokens)
::ProjectAccessTokenSerializer.new.represent(tokens, project: resource)
end
end
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
module Users
class NamespaceCalloutsController < Users::CalloutsController
private
def callout
Users::DismissNamespaceCalloutService.new(
container: nil, current_user: current_user, params: callout_params
).execute
end
def callout_params
params.permit(:namespace_id).merge(feature_name: feature_name)
end
end
end

View File

@ -14,5 +14,9 @@ module Projects
'required' => %w[project_id namespace_id root_namespace_id features]
}
end
def pages_related?
data[:features].include?("pages_access_level")
end
end
end

View File

@ -35,6 +35,36 @@ module Mutations
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :generic_duplicate_exception_regex)
argument :maven_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_package_requests_forwarding)
argument :npm_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :npm_package_requests_forwarding)
argument :pypi_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :pypi_package_requests_forwarding)
argument :lock_maven_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_maven_package_requests_forwarding)
argument :lock_npm_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_npm_package_requests_forwarding)
argument :lock_pypi_package_requests_forwarding,
GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_pypi_package_requests_forwarding)
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,

View File

@ -19,8 +19,7 @@ module Types
value 'ALL_AVAILABLE',
description:
"Include all runners. This list includes runners for all projects in the group " \
"and subgroups, as well as for the parent groups and instance. " \
"Will not return runners if `runners_finder_all_available` feature flag is disabled.",
"and subgroups, as well as for the parent groups and instance.",
value: :all_available,
deprecated: { milestone: '15.5', reason: :alpha }
end

View File

@ -8,9 +8,50 @@ module Types
authorize :admin_package
field :generic_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
field :generic_duplicates_allowed, GraphQL::Types::Boolean, null: false, description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
field :maven_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
field :maven_duplicates_allowed, GraphQL::Types::Boolean, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
field :generic_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
field :generic_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
field :maven_duplicate_exception_regex, Types::UntrustedRegexp,
null: true,
description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
field :maven_duplicates_allowed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
field :maven_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether Maven package forwarding is allowed for this namespace.'
field :npm_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether npm package forwarding is allowed for this namespace.'
field :pypi_package_requests_forwarding, GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether PyPI package forwarding is allowed for this namespace.'
field :lock_maven_package_requests_forwarding, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether Maven package forwarding is locked for all descendent namespaces.'
field :lock_npm_package_requests_forwarding, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether npm package forwarding is locked for all descendent namespaces.'
field :lock_pypi_package_requests_forwarding, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether PyPI package forwarding is locked for all descendent namespaces.'
field :maven_package_requests_forwarding_locked, GraphQL::Types::Boolean,
null: false,
method: :maven_package_requests_forwarding_locked?,
description: 'Indicates whether Maven package forwarding settings are locked by a parent namespace.'
field :npm_package_requests_forwarding_locked, GraphQL::Types::Boolean,
null: false,
method: :npm_package_requests_forwarding_locked?,
description: 'Indicates whether npm package forwarding settings are locked by a parent namespace.'
field :pypi_package_requests_forwarding_locked, GraphQL::Types::Boolean,
null: false,
method: :pypi_package_requests_forwarding_locked?,
description: 'Indicates whether PyPI package forwarding settings are locked by a parent namespace.'
end
end

View File

@ -16,6 +16,54 @@ module EventsHelper
'joined' => 'users'
}.freeze
def localized_action_name_map
{
accepted: s_('Event|accepted'),
approved: s_('Event|approved'),
closed: s_('Event|closed'),
'commented on': s_('Event|commented on'),
created: s_('Event|created'),
destroyed: s_('Event|destroyed'),
joined: s_('Event|joined'),
left: s_('Event|left'),
opened: s_('Event|opened'),
updated: s_('Event|updated'),
'removed due to membership expiration from': s_('Event|removed due to membership expiration from')
}.merge(localized_push_action_name_map,
localized_created_project_action_name_map,
localized_design_action_names
).freeze
end
def localized_push_action_name_map
{
'pushed new': s_('Event|pushed new'),
deleted: s_('Event|deleted'),
'pushed to': s_('Event|pushed to')
}.freeze
end
def localized_created_project_action_name_map
{
created: s_('Event|created'),
imported: s_('Event|imported')
}.freeze
end
def localized_design_action_names
{
added: s_('Event|added'),
updated: s_('Event|updated'),
removed: s_('Event|removed')
}.freeze
end
def localized_action_name(event)
action_name = event.action_name
# The action fallback is used to cover the types were not included in the maps.
localized_action_name_map[action_name.to_sym] || action_name
end
def link_to_author(event, self_added: false)
author = event.author

View File

@ -280,6 +280,7 @@ class Event < ApplicationRecord
"opened"
end
end
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/PerceivedComplexity
@ -447,9 +448,9 @@ class Event < ApplicationRecord
def design_action_names
{
created: _('added'),
updated: _('updated'),
destroyed: _('removed')
created: 'added',
updated: 'updated',
destroyed: 'removed'
}
end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Namespace::Detail < ApplicationRecord
include IgnorableColumns
ignore_column :free_user_cap_over_limt_notified_at, remove_with: '15.7', remove_after: '2022-11-22'
belongs_to :namespace, inverse_of: :namespace_details
validates :namespace, presence: true
validates :description, length: { maximum: 255 }

View File

@ -232,7 +232,6 @@ class User < ApplicationRecord
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :project_callouts, class_name: 'Users::ProjectCallout'
has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@ -2098,14 +2097,6 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
# Deprecated: do not use. See: https://gitlab.com/gitlab-org/gitlab/-/issues/371017
def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
source_feature_name = "#{feature_name}_#{namespace.id}"
callout = namespace_callouts_by_feature_name[source_feature_name]
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
def dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil)
callout = project_callouts.find_by(feature_name: feature_name, project: project)
@ -2138,11 +2129,6 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end
def find_or_initialize_namespace_callout(feature_name, namespace_id)
namespace_callouts
.find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
end
def find_or_initialize_project_callout(feature_name, project_id)
project_callouts
.find_or_initialize_by(feature_name: ::Users::ProjectCallout.feature_names[feature_name], project_id: project_id)
@ -2275,10 +2261,6 @@ class User < ApplicationRecord
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
end
def namespace_callouts_by_feature_name
@namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
end
def authorized_groups_without_shared_membership
Group.from_union(
[

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
module Users
class NamespaceCallout < ApplicationRecord
include Users::Calloutable
self.table_name = 'user_namespace_callouts'
belongs_to :namespace
enum feature_name: {
invite_members_banner: 1,
approaching_seat_count_threshold: 2, # EE-only
storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only
storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only
storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only
storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only
preview_user_over_limit_free_plan_alert: 7, # EE-only
user_reached_limit_free_plan_alert: 8, # EE-only
web_hook_disabled: 9
}
validates :namespace, presence: true
validates :feature_name,
presence: true,
uniqueness: { scope: [:user_id, :namespace_id] },
inclusion: { in: NamespaceCallout.feature_names.keys }
def source_feature_name
"#{feature_name}_#{namespace_id}"
end
end
end

View File

@ -11,7 +11,11 @@ module Users
enum feature_name: {
awaiting_members_banner: 1, # EE-only
web_hook_disabled: 2,
ultimate_feature_removal_banner: 3
ultimate_feature_removal_banner: 3,
storage_enforcement_banner_first_enforcement_threshold: 4, # EE-only
storage_enforcement_banner_second_enforcement_threshold: 5, # EE-only
storage_enforcement_banner_third_enforcement_threshold: 6, # EE-only
storage_enforcement_banner_fourth_enforcement_threshold: 7 # EE-only
}
validates :project, presence: true

View File

@ -87,10 +87,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
end
condition(:runners_finder_all_available, scope: :subject) do
Feature.enabled?(:runners_finder_all_available, group)
end
rule { can?(:read_group) & design_management_enabled }.policy do
enable :read_design_activity
end
@ -158,8 +154,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :read_group_all_available_runners
end
rule { ~runners_finder_all_available }.prevent :read_group_all_available_runners
rule { reporter }.policy do
enable :reporter_access
enable :read_container_image

View File

@ -11,7 +11,7 @@ class GroupAccessTokenEntity < AccessTokenEntityBase
revoke_group_settings_access_token_path(
id: token,
group_id: group.path)
group_id: group.full_path)
end
expose :role do |token, options|

View File

@ -11,7 +11,7 @@ class ProjectAccessTokenEntity < AccessTokenEntityBase
revoke_namespace_project_settings_access_token_path(
id: token,
namespace_id: project.namespace.path,
namespace_id: project.namespace.full_path,
project_id: project.path)
end

View File

@ -8,7 +8,13 @@ module Namespaces
ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed
maven_duplicate_exception_regex
generic_duplicates_allowed
generic_duplicate_exception_regex].freeze
generic_duplicate_exception_regex
maven_package_requests_forwarding
npm_package_requests_forwarding
pypi_package_requests_forwarding
lock_maven_package_requests_forwarding
lock_npm_package_requests_forwarding
lock_pypi_package_requests_forwarding].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module Users
class DismissNamespaceCalloutService < DismissCalloutService
private
def callout
current_user.find_or_initialize_namespace_callout(params[:feature_name], params[:namespace_id])
end
end
end

View File

@ -6,7 +6,7 @@
= inline_event_icon(event)
- if event.target
%span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
= event.action_name
= localized_action_name(event)
%span.event-target-type.gl-mr-2= event.target_type_name
= link_to event_target_path(event), class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do
= event.target.reference_link_text

View File

@ -24,13 +24,11 @@
= _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- if @new_resource_access_token
= render 'shared/access_tokens/created_container',
type: type,
new_token_value: @new_resource_access_token
#js-new-access-token-app{ data: { access_token_type: type } }
- if current_user.can?(:create_resource_access_tokens, @group)
= render 'shared/access_tokens/form',
ajax: true,
type: type,
path: group_settings_access_tokens_path(@group),
resource: @group,
@ -41,10 +39,6 @@
prefix: :resource_access_token,
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
= render 'shared/access_tokens/table',
active_tokens: @active_resource_access_tokens,
resource: @group,
type: type,
type_plural: type_plural,
revoke_route_helper: ->(token) { revoke_group_settings_access_token_path(id: token) },
no_active_tokens_message: _('This group has no active access tokens.')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true
} }

View File

@ -24,13 +24,11 @@
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- if @new_resource_access_token
= render 'shared/access_tokens/created_container',
type: type,
new_token_value: @new_resource_access_token
#js-new-access-token-app{ data: { access_token_type: type } }
- if current_user.can?(:create_resource_access_tokens, @project)
= render 'shared/access_tokens/form',
ajax: true,
type: type,
path: project_settings_access_tokens_path(@project),
resource: @project,
@ -42,10 +40,5 @@
description_prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
= render 'shared/access_tokens/table',
active_tokens: @active_resource_access_tokens,
resource: @project,
type: type,
type_plural: type_plural,
revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
no_active_tokens_message: _('This project has no active access tokens.')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true
} }

View File

@ -1020,6 +1020,24 @@
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_attachments_import_issue
:worker_name: Gitlab::GithubImport::Attachments::ImportIssueWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_attachments_import_merge_request
:worker_name: Gitlab::GithubImport::Attachments::ImportMergeRequestWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_attachments_import_note
:worker_name: Gitlab::GithubImport::Attachments::ImportNoteWorker
:feature_category: :importers

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Attachments
class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
Representation::NoteText
end
def importer_class
Importer::NoteAttachmentsImporter
end
def object_type
:issue_attachment
end
end
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Attachments
class ImportMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
Representation::NoteText
end
def importer_class
Importer::NoteAttachmentsImporter
end
def object_type
:merge_request_attachment
end
end
end
end
end

View File

@ -30,7 +30,9 @@ module Gitlab
def importers
[
::Gitlab::GithubImport::Importer::Attachments::ReleasesImporter,
::Gitlab::GithubImport::Importer::Attachments::NotesImporter
::Gitlab::GithubImport::Importer::Attachments::NotesImporter,
::Gitlab::GithubImport::Importer::Attachments::IssuesImporter,
::Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter
]
end

View File

@ -1,8 +0,0 @@
---
name: runners_finder_all_available
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96770
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/374525
milestone: '15.5'
type: development
group: group::runner
default_enabled: false

View File

@ -4,17 +4,33 @@ return unless Gitlab::Runtime.application?
return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'])
Gitlab::Cluster::LifecycleEvents.on_worker_start do
handler =
if Gitlab::Runtime.puma?
Gitlab::Memory::Watchdog::PumaHandler.new
elsif Gitlab::Runtime.sidekiq?
Gitlab::Memory::Watchdog::TermProcessHandler.new
else
Gitlab::Memory::Watchdog::NullHandler.instance
end
watchdog = Gitlab::Memory::Watchdog.new
max_strikes = ENV.fetch('GITLAB_MEMWD_MAX_STRIKES', 5).to_i
sleep_time_seconds = ENV.fetch('GITLAB_MEMWD_SLEEP_TIME_SEC', 60).to_i
max_mem_growth = ENV.fetch('GITLAB_MEMWD_MAX_MEM_GROWTH', 3.0).to_f
max_heap_frag = ENV.fetch('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.5).to_f
watchdog.configure do |config|
config.handler =
if Gitlab::Runtime.puma?
Gitlab::Memory::Watchdog::PumaHandler.new
elsif Gitlab::Runtime.sidekiq?
Gitlab::Memory::Watchdog::TermProcessHandler.new
else
Gitlab::Memory::Watchdog::NullHandler.instance
end
config.logger = Gitlab::AppLogger
config.sleep_time_seconds = sleep_time_seconds
# config.monitor.use MonitorClass, args*, max_strikes:, kwargs**, &block
config.monitors.use Gitlab::Memory::Watchdog::Monitor::HeapFragmentation,
max_heap_fragmentation: max_heap_frag,
max_strikes: max_strikes
config.monitors.use Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth,
max_mem_growth: max_mem_growth,
max_strikes: max_strikes
end
watchdog = Gitlab::Memory::Watchdog.new(
handler: handler, logger: Gitlab::AppLogger
)
Gitlab::BackgroundTask.new(watchdog).start
end

View File

@ -64,7 +64,6 @@ scope '-/users', module: :users do
end
resources :callouts, only: [:create]
resources :namespace_callouts, only: [:create]
resources :group_callouts, only: [:create]
resources :project_callouts, only: [:create]
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class DropNotesNoteTrigramIndex < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_notes_on_note_gin_trigram'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :notes, INDEX_NAME
end
def down
# no-op
# we never want to add this index back since it doesn't exist in production
# we are only using this migration to cleanup other environments where this index does exist
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddAsyncIndexAuthorIdTargetProjectIdOnMergeRequests < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_merge_requests_on_author_id_and_id'
disable_ddl_transaction!
def up
prepare_async_index :merge_requests, %i[author_id id], name: INDEX_NAME
end
def down
unprepare_async_index :merge_requests, %i[author_id id], name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
1275aff394d75cc254e664a81f52880bc248343dad7a07162973cafe268d40e6

View File

@ -0,0 +1 @@
2ac315a49a5026938abc21a98974fd42b39b7535d86530085a01fc7f5687bb0e

View File

@ -59,6 +59,8 @@ verification methods:
| Blobs | Pages _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | CI Secure Files _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | CI Secure Files _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | Incident Metric Images _(file system)_ | Geo with API/Managed | SHA256 checksum |
| Blobs | Incident Metric Images _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo sites.
- (*2*): Object storage replication can be performed by Geo or by your object storage provider/appliance
@ -204,9 +206,9 @@ successfully, you must replicate their data using some other means.
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | **Yes** (13.12) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|[Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
|[Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | No | No | |
| [GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
| [Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | Yes | Yes | Replication/Verification is handled via the Uploads data type |
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | No | No | |
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|[Elasticsearch integration](../../../integration/advanced_search/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |

View File

@ -683,6 +683,9 @@ ApplicationSetting.current
### Open object in `irb`
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
Sometimes it is easier to go through a method if you are in the context of the object. You can shim into the namespace of `Object` to let you open `irb` in the context of any object:
```ruby

View File

@ -31,6 +31,7 @@ The following lists the currently supported OSs and their possible EOL dates.
| Ubuntu 20.04 | GitLab CE / GitLab EE 13.2.0 | amd64, arm64 | [Ubuntu Install Documentation](https://about.gitlab.com/install/#ubuntu) | April 2025 | <https://wiki.ubuntu.com/Releases> |
| Amazon Linux 2 | GitLab CE / GitLab EE 14.9.0 | amd64, arm64 | [Amazon Linux 2 Install Documentation](https://about.gitlab.com/install/#amazonlinux-2) | June 2023 | <https://aws.amazon.com/amazon-linux-2/faqs/> |
| Raspberry Pi OS (Buster) (formerly known as Raspbian Buster) | GitLab CE 12.2.0 | armhf | [Raspberry Pi Install Documentation](https://about.gitlab.com/install/#raspberry-pi-os) | 2024 | [Raspberry Pi Details](https://www.raspberrypi.com/news/new-old-functionality-with-raspberry-pi-os-legacy/) |
| Raspberry Pi OS (Bullseye) | GitLab CE 15.5.0 | armhf | [Raspberry Pi Install Documentation](https://about.gitlab.com/install/#raspberry-pi-os) | 2026 | [Raspberry Pi Details](https://www.raspberrypi.com/news/raspberry-pi-os-debian-bullseye/) |
NOTE:
CentOS 8 was EOL on December 31, 2021. In GitLab 14.5 and later,

View File

@ -185,8 +185,7 @@ as most of the fixes are relatively high risk, involving running code on the Rai
### Read only projects
If you have [set projects read only](../troubleshooting/gitlab_rails_cheat_sheet.md#make-a-project-read-only-can-only-be-done-in-the-console)
they might fail to migrate.
If you have set projects as read only they might fail to migrate.
1. [Start a Rails console](../operations/rails_console.md#starting-a-rails-console-session).
@ -233,7 +232,7 @@ Delete the project using the Rails console:
- Replace `admin_handle` with the handle of an instance administrator or with `root`.
- Verify the output before proceeding. **There are no other checks performed**.
1. [Destroy the project](../troubleshooting/gitlab_rails_cheat_sheet.md#destroy-a-project) **immediately**:
1. [Destroy the project](../../user/project/working_with_projects.md#delete-a-project-using-console) **immediately**:
```ruby
Projects::DestroyService.new(project, user).execute

View File

@ -75,114 +75,6 @@ Benchmark.bm do |x|
end
```
## Projects
### Clear a project's cache
```ruby
ProjectCacheWorker.perform_async(project.id)
```
### Expire the .exists? cache
```ruby
project.repository.expire_exists_cache
```
### Make all projects private
```ruby
Project.update_all(visibility_level: 0)
```
### Find projects that are pending deletion
```ruby
#
# This section lists all the projects which are pending deletion
#
projects = Project.where(pending_delete: true)
projects.each do |p|
puts "Project ID: #{p.id}"
puts "Project name: #{p.name}"
puts "Repository path: #{p.repository.full_path}"
end
#
# Assign a user (the root user does)
#
user = User.find_by_username('root')
#
# For each project listed repeat these two commands
#
# Find the project, update the xxx-changeme values from above
project = Project.find_by_full_path('group-changeme/project-changeme')
# Immediately delete the project
::Projects::DestroyService.new(project, user, {}).execute
```
### Destroy a project
```ruby
project = Project.find_by_full_path('<project_path>')
user = User.find_by_username('<username>')
ProjectDestroyWorker.perform_async(project.id, user.id, {})
# or ProjectDestroyWorker.new.perform(project.id, user.id, {})
# or Projects::DestroyService.new(project, user).execute
```
If this fails, display why it doesn't work with:
```ruby
project = Project.find_by_full_path('<project_path>')
project.delete_error
```
### Remove fork relationship manually
```ruby
p = Project.find_by_full_path('<project_path>')
u = User.find_by_username('<username>')
::Projects::UnlinkForkService.new(p, u).execute
```
### Make a project read-only (can only be done in the console)
```ruby
# Make a project read-only
project.repository_read_only = true; project.save
# OR
project.update!(repository_read_only: true)
```
### Transfer project from one namespace to another
```ruby
p = Project.find_by_full_path('<project_path>')
# To set the owner of the project
current_user= p.creator
# Namespace where you want this to be moved.
namespace = Namespace.find_by_full_path("<new_namespace>")
::Projects::TransferService.new(p, current_user).execute(namespace)
```
### Find projects using an SQL query
Find and store an array of projects based on an SQL query:
```ruby
# Finds projects that end with '%ject'
projects = Project.find_by_sql("SELECT * FROM projects WHERE name LIKE '%ject'")
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
```
## Imports and exports
### Import a project

View File

@ -5408,9 +5408,15 @@ Input type: `UpdateNamespacePackageSettingsInput`
| <a id="mutationupdatenamespacepackagesettingsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationupdatenamespacepackagesettingsgenericduplicateexceptionregex"></a>`genericDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| <a id="mutationupdatenamespacepackagesettingsgenericduplicatesallowed"></a>`genericDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate generic packages are allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingslockmavenpackagerequestsforwarding"></a>`lockMavenPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether Maven package forwarding is locked for all descendent namespaces. |
| <a id="mutationupdatenamespacepackagesettingslocknpmpackagerequestsforwarding"></a>`lockNpmPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether npm package forwarding is locked for all descendent namespaces. |
| <a id="mutationupdatenamespacepackagesettingslockpypipackagerequestsforwarding"></a>`lockPypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is locked for all descendent namespaces. |
| <a id="mutationupdatenamespacepackagesettingsmavenduplicateexceptionregex"></a>`mavenDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| <a id="mutationupdatenamespacepackagesettingsmavenduplicatesallowed"></a>`mavenDuplicatesAllowed` | [`Boolean`](#boolean) | Indicates whether duplicate Maven packages are allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingsmavenpackagerequestsforwarding"></a>`mavenPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether Maven package forwarding is allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingsnamespacepath"></a>`namespacePath` | [`ID!`](#id) | Namespace path where the namespace package setting is located. |
| <a id="mutationupdatenamespacepackagesettingsnpmpackagerequestsforwarding"></a>`npmPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether npm package forwarding is allowed for this namespace. |
| <a id="mutationupdatenamespacepackagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. |
#### Fields
@ -15759,8 +15765,17 @@ Namespace-level Package Registry settings.
| ---- | ---- | ----------- |
| <a id="packagesettingsgenericduplicateexceptionregex"></a>`genericDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| <a id="packagesettingsgenericduplicatesallowed"></a>`genericDuplicatesAllowed` | [`Boolean!`](#boolean) | Indicates whether duplicate generic packages are allowed for this namespace. |
| <a id="packagesettingslockmavenpackagerequestsforwarding"></a>`lockMavenPackageRequestsForwarding` | [`Boolean!`](#boolean) | Indicates whether Maven package forwarding is locked for all descendent namespaces. |
| <a id="packagesettingslocknpmpackagerequestsforwarding"></a>`lockNpmPackageRequestsForwarding` | [`Boolean!`](#boolean) | Indicates whether npm package forwarding is locked for all descendent namespaces. |
| <a id="packagesettingslockpypipackagerequestsforwarding"></a>`lockPypiPackageRequestsForwarding` | [`Boolean!`](#boolean) | Indicates whether PyPI package forwarding is locked for all descendent namespaces. |
| <a id="packagesettingsmavenduplicateexceptionregex"></a>`mavenDuplicateExceptionRegex` | [`UntrustedRegexp`](#untrustedregexp) | When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| <a id="packagesettingsmavenduplicatesallowed"></a>`mavenDuplicatesAllowed` | [`Boolean!`](#boolean) | Indicates whether duplicate Maven packages are allowed for this namespace. |
| <a id="packagesettingsmavenpackagerequestsforwarding"></a>`mavenPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether Maven package forwarding is allowed for this namespace. |
| <a id="packagesettingsmavenpackagerequestsforwardinglocked"></a>`mavenPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether Maven package forwarding settings are locked by a parent namespace. |
| <a id="packagesettingsnpmpackagerequestsforwarding"></a>`npmPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether npm package forwarding is allowed for this namespace. |
| <a id="packagesettingsnpmpackagerequestsforwardinglocked"></a>`npmPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether npm package forwarding settings are locked by a parent namespace. |
| <a id="packagesettingspypipackagerequestsforwarding"></a>`pypiPackageRequestsForwarding` | [`Boolean`](#boolean) | Indicates whether PyPI package forwarding is allowed for this namespace. |
| <a id="packagesettingspypipackagerequestsforwardinglocked"></a>`pypiPackageRequestsForwardingLocked` | [`Boolean!`](#boolean) | Indicates whether PyPI package forwarding settings are locked by a parent namespace. |
### `PackageTag`
@ -19987,7 +20002,7 @@ Values for filtering runners in namespaces. The previous type name `RunnerMember
| Value | Description |
| ----- | ----------- |
| <a id="cirunnermembershipfilterall_available"></a>`ALL_AVAILABLE` **{warning-solid}** | **Introduced** in 15.5. This feature is in Alpha. It can be changed or removed at any time. Include all runners. This list includes runners for all projects in the group and subgroups, as well as for the parent groups and instance. Will not return runners if `runners_finder_all_available` feature flag is disabled. |
| <a id="cirunnermembershipfilterall_available"></a>`ALL_AVAILABLE` **{warning-solid}** | **Introduced** in 15.5. This feature is in Alpha. It can be changed or removed at any time. Include all runners. This list includes runners for all projects in the group and subgroups, as well as for the parent groups and instance. |
| <a id="cirunnermembershipfilterdescendants"></a>`DESCENDANTS` | Include runners that have either a direct or inherited relationship. These runners can be specific to a project or a group. |
| <a id="cirunnermembershipfilterdirect"></a>`DIRECT` | Include runners that have a direct relationship. |

View File

@ -194,10 +194,7 @@ From this page, you can edit, pause, and remove runners from the group, its subg
#### Filter group runners to show only inherited
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337838/) in GitLab 15.5, [with a flag](../../administration/feature_flags.md) named `runners_finder_all_available`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `runners_finder_all_available`. On GitLab.com, this feature is not available.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337838/) in GitLab 15.5.
You can choose to show all runners in the list, or show only
those that are inherited from the instance or other groups.

View File

@ -135,6 +135,8 @@ For each entity with Markdown text in the project, we schedule a job of:
- `Gitlab::GithubImport::ImportReleaseAttachmentsWorker` for every release.
- `Gitlab::GithubImport::ImportNoteAttachmentsWorker` for every note.
- `Gitlab::GithubImport::ImportIssueAttachmentsWorker` for every issue.
- `Gitlab::GithubImport::ImportMergeRequestAttachmentsWorker` for every merge request.
Each job:

View File

@ -71,7 +71,7 @@ and issue creation. Your instance becomes read-only and
an expiration message displays to all administrators. You have a 14-day grace period
before this occurs.
To resume functionality, [renew your subscription](../../subscriptions/self_managed/index.md#renew-a-subscription).
To resume functionality, [renew your subscription](../../subscriptions/self_managed/index.md#renew-a-subscription).
If the license has been expired for more than 30 days, you must purchase a [new subscription](../../subscriptions/self_managed/index.md) to resume functionality.

View File

@ -196,6 +196,8 @@ The following items of a project are imported:
- Attachments for:
- Release notes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15620) in GitLab 15.4.
- Comments and notes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18052) in GitLab 15.5.
- Issue description. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18052) in GitLab 15.5.
- Merge Request description. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18052) in GitLab 15.5.
All attachment imports are disabled by default behind
`github_importer_attachments_import` [feature flag](../../../administration/feature_flags.md). From GitLab 15.5, can be imported

View File

@ -376,3 +376,33 @@ Configure Error Tracking to discover and view [Sentry errors within GitLab](../.
[Add Storage credentials](../../../operations/incident_management/status_page.md#sync-incidents-to-the-status-page)
to enable the syncing of public Issues to a [deployed status page](../../../operations/incident_management/status_page.md#create-a-status-page-project).
## Troubleshooting
When working with project settings, you might encounter the following issues, or require alternate methods to complete specific tasks.
### Remove a fork relationship through console
If removing the fork through the UI or API is not working, you can attempt the fork relationship removal in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
```ruby
p = Project.find_by_full_path('<project_path>')
u = User.find_by_username('<username>')
Projects::UnlinkForkService.new(p, u).execute
```
### Transfer a project through console
If transferring a project through the UI or API is not working, you can attempt the transfer in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
```ruby
p = Project.find_by_full_path('<project_path>')
# To set the owner of the project
current_user = p.creator
# Namespace where you want this to be moved
namespace = Namespace.find_by_full_path("<new_namespace>")
Projects::TransferService.new(p, current_user).execute(namespace)
```

View File

@ -90,8 +90,7 @@ To create a blank project:
- In the **Project slug** field, enter the path to your project. The GitLab instance uses the
slug as the URL path to the project. To change the slug, first enter the project name,
then change the slug.
- In the **Project description (optional)** field, enter the description of your project's dashboard.
- In the **Project target (optional)** field, select your project's deployment target.
- In the **Project deployment target (optional)** field, select your project's deployment target.
This information helps GitLab better understand its users and their deployment requirements.
- To modify the project's [viewing and access rights](../public_access.md) for
users, change the **Visibility Level**.
@ -496,3 +495,67 @@ download starts, the `insteadOf` configuration sends the traffic to the secondar
- [Fork a project](repository/forking_workflow.md#creating-a-fork).
- [Adjust project visibility and access levels](settings/index.md#configure-project-visibility-features-and-permissions).
- [Limitations on project and group names](../../user/reserved_names.md#limitations-on-project-and-group-names)
## Troubleshooting
When working with projects, you might encounter the following issues, or require alternate methods to complete specific tasks.
### Find projects using an SQL query
While in [a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session), you can find and store an array of projects based on a SQL query:
```ruby
# Finds projects that end with '%ject'
projects = Project.find_by_sql("SELECT * FROM projects WHERE name LIKE '%ject'")
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
```
### Clear a project's or repository's cache
If a project or repository has been updated but the state is not reflected in the UI, you may need to clear the project's or repository's cache.
You can do so through [a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session) and one of the following:
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
```ruby
## Clear project cache
ProjectCacheWorker.perform_async(project.id)
## Clear repository .exists? cache
project.repository.expire_exists_cache
```
### Find projects that are pending deletion
If you need to find all projects marked for deletion but that have not yet been deleted,
[start a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session) and run the following:
```ruby
projects = Project.where(pending_delete: true)
projects.each do |p|
puts "Project ID: #{p.id}"
puts "Project name: #{p.name}"
puts "Repository path: #{p.repository.full_path}"
end
```
### Delete a project using console
If a project cannot be deleted, you can attempt to delete it through [Rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session).
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
```ruby
project = Project.find_by_full_path('<project_path>')
user = User.find_by_username('<username>')
ProjectDestroyWorker.new.perform(project.id, user.id, {})
```
If this fails, display why it doesn't work with:
```ruby
project = Project.find_by_full_path('<project_path>')
project.delete_error
```

View File

@ -44,9 +44,10 @@ module API
end
get ":channel/index.yaml" do
authorize_read_package!(authorized_user_project)
project = authorized_user_project(action: :read_package)
authorize_read_package!(project)
packages = Packages::Helm::PackagesFinder.new(authorized_user_project, params[:channel]).execute
packages = Packages::Helm::PackagesFinder.new(project, params[:channel]).execute
env['api.format'] = :yaml
present ::Packages::Helm::IndexPresenter.new(params[:id], params[:channel], packages),
@ -61,11 +62,12 @@ module API
requires :file_name, type: String, desc: 'Helm package file name'
end
get ":channel/charts/:file_name.tgz" do
authorize_read_package!(authorized_user_project)
project = authorized_user_project(action: :read_package)
authorize_read_package!(project)
package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent!
package_file = Packages::Helm::PackageFilesFinder.new(project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent!
track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace)
track_package_event('pull_package', :helm, project: project, namespace: project.namespace)
present_package_file!(package_file)
end

View File

@ -8,6 +8,11 @@ module Bitbucket
@connection = Connection.new(options)
end
def last_issue(repo)
parsed_response = connection.get("/repositories/#{repo}/issues?pagelen=1&sort=-created_on&state=ALL")
Bitbucket::Representation::Issue.new(parsed_response['values'].first)
end
def issues(repo)
path = "/repositories/#{repo}/issues"
get_collection(path, :issue)

View File

@ -67,6 +67,14 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
def allocate_issues_internal_id!(project, client)
last_bitbucket_issue = client.last_issue(repo)
return unless last_bitbucket_issue
Issue.track_project_iid!(project, last_bitbucket_issue.iid)
end
def repo
@repo ||= client.repo(project.import_source)
end
@ -84,6 +92,10 @@ module Gitlab
def import_issues
return unless repo.issues_enabled?
# If a user creates an issue while the import is in progress, this can lead to an import failure.
# The workaround is to allocate IIDs before starting the importer.
allocate_issues_internal_id!(project, client)
create_labels
issue_type_id = WorkItems::Type.default_issue_type.id

View File

@ -46,6 +46,9 @@ module Gitlab
store.subscribe ::Pages::InvalidateDomainCacheWorker,
to: ::Projects::ProjectAttributesChangedEvent,
if: -> (event) { event.pages_related? }
store.subscribe ::Pages::InvalidateDomainCacheWorker,
to: ::Projects::ProjectFeaturesChangedEvent,
if: -> (event) { event.pages_related? }
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupDeletedEvent

View File

@ -12,7 +12,7 @@ module Gitlab
# The method that will be called for traversing through all the objects to
# import, yielding them to the supplied block.
def each_object_to_import
collection.each_batch(of: BATCH_SIZE) do |batch|
collection.each_batch(of: BATCH_SIZE, column: ordering_column) do |batch|
batch.each do |record|
next if already_imported?(record)
@ -41,6 +41,10 @@ module Gitlab
raise Gitlab::GithubImport::Exceptions::NotImplementedError, '#collection'
end
def ordering_column
:id
end
def object_representation(object)
representation_class.from_db_record(object)
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Attachments
class IssuesImporter < ::Gitlab::GithubImport::Importer::Attachments::BaseImporter
def sidekiq_worker_class
::Gitlab::GithubImport::Attachments::ImportIssueWorker
end
def collection_method
:issue_attachments
end
def object_type
:issue_attachment
end
def id_for_already_imported_cache(issue)
issue.id
end
private
def collection
project.issues.select(:id, :description)
end
def ordering_column
:iid
end
end
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Attachments
class MergeRequestsImporter < ::Gitlab::GithubImport::Importer::Attachments::BaseImporter
def sidekiq_worker_class
::Gitlab::GithubImport::Attachments::ImportMergeRequestWorker
end
def collection_method
:merge_request_attachments
end
def object_type
:merge_request_attachment
end
def id_for_already_imported_cache(merge_request)
merge_request.id
end
private
def collection
project.merge_requests.select(:id, :description)
end
def ordering_column
:iid
end
end
end
end
end
end

View File

@ -43,6 +43,10 @@ module Gitlab
case note_text.record_type
when ::Release.name
::Release.find(note_text.record_db_id).update_column(:description, text)
when ::Issue.name
::Issue.find(note_text.record_db_id).update_column(:description, text)
when ::MergeRequest.name
::MergeRequest.find(note_text.record_db_id).update_column(:description, text)
when ::Note.name
::Note.find(note_text.record_db_id).update_column(:note, text)
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
# This class only partly represents Release record from DB and
# This class only partly represents MODELS_ALLOWLIST records from DB and
# is used to connect ReleasesAttachmentsImporter, NotesAttachmentsImporter etc.
# with NoteAttachmentsImporter without modifying ObjectImporter a lot.
# Attachments are inside release's `description`.
@ -11,7 +11,7 @@ module Gitlab
include ToHash
include ExposeAttribute
MODELS_WHITELIST = [::Release, ::Note].freeze
MODELS_ALLOWLIST = [::Release, ::Note, ::Issue, ::MergeRequest].freeze
ModelNotSupported = Class.new(StandardError)
attr_reader :attributes
@ -21,12 +21,13 @@ module Gitlab
class << self
# Builds a note text representation from DB record of Note or Release.
#
# record - An instance of `Release` or `Note` model.
# record - An instance of `Note`, `Release`, `Issue`, `MergeRequest` model
def from_db_record(record)
check_record_class!(record)
record_type = record.class.name
text = record.is_a?(Release) ? record.description : record.note
# only column for note is different along MODELS_ALLOWLIST
text = record.is_a?(::Note) ? record.note : record.description
new(
record_db_id: record.id,
record_type: record_type,
@ -41,7 +42,7 @@ module Gitlab
private
def check_record_class!(record)
raise ModelNotSupported, record.class.name if MODELS_WHITELIST.exclude?(record.class)
raise ModelNotSupported, record.class.name if MODELS_ALLOWLIST.exclude?(record.class)
end
end

View File

@ -2,25 +2,10 @@
module Gitlab
module Memory
# A background thread that observes Ruby heap fragmentation and calls
# into a handler when the Ruby heap has been fragmented for an extended
# period of time.
#
# See Gitlab::Metrics::Memory for how heap fragmentation is defined.
#
# To decide whether a given fragmentation level is being exceeded,
# the watchdog regularly polls the GC. Whenever a violation occurs
# a strike is issued. If the maximum number of strikes are reached,
# a handler is invoked to deal with the situation.
#
# The duration for which a process may be above a given fragmentation
# threshold is computed as `max_strikes * sleep_time_seconds`.
# A background thread that monitors Ruby memory and calls
# into a handler when the Ruby process violates defined limits
# for an extended period of time.
class Watchdog
DEFAULT_SLEEP_TIME_SECONDS = 60 * 5
DEFAULT_MAX_HEAP_FRAG = 0.5
DEFAULT_MAX_MEM_GROWTH = 3.0
DEFAULT_MAX_STRIKES = 5
# This handler does nothing. It returns `false` to indicate to the
# caller that the situation has not been dealt with so it will
# receive calls repeatedly if fragmentation remains high.
@ -62,73 +47,27 @@ module Gitlab
end
end
# max_heap_fragmentation:
# The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
# max_mem_growth:
# A multiplier for how much excess private memory a worker can map compared to a reference process
# (itself or the primary in a pre-fork server.)
# max_strikes:
# How many times the process is allowed to be above max_heap_fragmentation before
# a handler is invoked.
# sleep_time_seconds:
# Used to control the frequency with which the watchdog will wake up and poll the GC.
def initialize(
handler: NullHandler.instance,
logger: Logger.new($stdout),
max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_MAX_HEAP_FRAG,
max_mem_growth: ENV['GITLAB_MEMWD_MAX_MEM_GROWTH']&.to_f || DEFAULT_MAX_MEM_GROWTH,
max_strikes: ENV['GITLAB_MEMWD_MAX_STRIKES']&.to_i || DEFAULT_MAX_STRIKES,
sleep_time_seconds: ENV['GITLAB_MEMWD_SLEEP_TIME_SEC']&.to_i || DEFAULT_SLEEP_TIME_SECONDS,
**options)
super(**options)
@handler = handler
@logger = logger
@sleep_time_seconds = sleep_time_seconds
@max_strikes = max_strikes
@stats = {
heap_frag: {
max: max_heap_fragmentation,
strikes: 0
},
mem_growth: {
max: max_mem_growth,
strikes: 0
}
}
def initialize
@configuration = Configuration.new
@alive = true
init_prometheus_metrics(max_heap_fragmentation)
init_prometheus_metrics
end
attr_reader :max_strikes, :sleep_time_seconds
def max_heap_fragmentation
@stats[:heap_frag][:max]
end
def max_mem_growth
@stats[:mem_growth][:max]
end
def strikes(stat)
@stats[stat][:strikes]
def configure
yield @configuration
end
def call
@logger.info(log_labels.merge(message: 'started'))
logger.info(log_labels.merge(message: 'started'))
while @alive
sleep(@sleep_time_seconds)
sleep(sleep_time_seconds)
next unless Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
monitor_heap_fragmentation
monitor_memory_growth
monitor if Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
end
@logger.info(log_labels.merge(message: 'stopped'))
logger.info(log_labels.merge(message: 'stopped'))
end
def stop
@ -137,71 +76,24 @@ module Gitlab
private
def monitor_memory_condition(stat_key)
return unless @alive
def monitor
@configuration.monitors.call_each do |result|
break unless @alive
stat = @stats[stat_key]
next unless result.threshold_violated?
ok, labels = yield(stat)
@counter_violations.increment(reason: result.monitor_name)
if ok
stat[:strikes] = 0
else
stat[:strikes] += 1
@counter_violations.increment(reason: stat_key.to_s)
end
next unless result.strikes_exceeded?
if stat[:strikes] > @max_strikes
@alive = !memory_limit_exceeded_callback(stat_key, labels)
stat[:strikes] = 0
@alive = !memory_limit_exceeded_callback(result.monitor_name, result.payload)
end
end
def monitor_heap_fragmentation
monitor_memory_condition(:heap_frag) do |stat|
heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
[
heap_fragmentation <= stat[:max],
{
message: 'heap fragmentation limit exceeded',
memwd_cur_heap_frag: heap_fragmentation,
memwd_max_heap_frag: stat[:max]
}
]
end
end
def monitor_memory_growth
monitor_memory_condition(:mem_growth) do |stat|
worker_uss = Gitlab::Metrics::System.memory_usage_uss_pss[:uss]
reference_uss = reference_mem[:uss]
memory_limit = stat[:max] * reference_uss
[
worker_uss <= memory_limit,
{
message: 'memory limit exceeded',
memwd_uss_bytes: worker_uss,
memwd_ref_uss_bytes: reference_uss,
memwd_max_uss_bytes: memory_limit
}
]
end
end
# On pre-fork systems this would be the primary process memory from which workers fork.
# Otherwise it is the current process' memory.
#
# We initialize this lazily because in the initializer the application may not have
# finished booting yet, which would yield an incorrect baseline.
def reference_mem
@reference_mem ||= Gitlab::Metrics::System.memory_usage_uss_pss(pid: Gitlab::Cluster::PRIMARY_PID)
end
def memory_limit_exceeded_callback(stat_key, handler_labels)
all_labels = log_labels.merge(handler_labels)
.merge(memwd_cur_strikes: strikes(stat_key))
@logger.warn(all_labels)
@counter_violations_handled.increment(reason: stat_key.to_s)
def memory_limit_exceeded_callback(monitor_name, monitor_payload)
all_labels = log_labels.merge(monitor_payload)
logger.warn(all_labels)
@counter_violations_handled.increment(reason: monitor_name)
handler.call
end
@ -211,7 +103,15 @@ module Gitlab
# all that happens is we collect logs and Prometheus events for fragmentation violations.
return NullHandler.instance unless Feature.enabled?(:enforce_memory_watchdog, type: :ops)
@handler
@configuration.handler
end
def logger
@configuration.logger
end
def sleep_time_seconds
@configuration.sleep_time_seconds
end
def log_labels
@ -219,27 +119,20 @@ module Gitlab
pid: $$,
worker_id: worker_id,
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: @sleep_time_seconds,
memwd_max_strikes: @max_strikes,
memwd_sleep_time_s: sleep_time_seconds,
memwd_rss_bytes: process_rss_bytes
}
end
def worker_id
::Prometheus::PidProvider.worker_id
end
def process_rss_bytes
Gitlab::Metrics::System.memory_usage_rss
end
def init_prometheus_metrics(max_heap_fragmentation)
@heap_frag_limit = Gitlab::Metrics.gauge(
:gitlab_memwd_heap_frag_limit,
'The configured limit for how fragmented the Ruby heap is allowed to be'
)
@heap_frag_limit.set({}, max_heap_fragmentation)
def worker_id
::Prometheus::PidProvider.worker_id
end
def init_prometheus_metrics
default_labels = { pid: worker_id }
@counter_violations = Gitlab::Metrics.counter(
:gitlab_memwd_violations_total,

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Gitlab
module Memory
class Watchdog
class Configuration
class MonitorStack
def initialize
@monitors = []
end
def use(monitor_class, *args, **kwargs, &block)
remove(monitor_class)
@monitors.push(build_monitor_state(monitor_class, *args, **kwargs, &block))
end
def call_each
@monitors.each do |monitor|
yield monitor.call
end
end
private
def remove(monitor_class)
@monitors.delete_if { |monitor| monitor.monitor_class == monitor_class }
end
def build_monitor_state(monitor_class, *args, max_strikes:, **kwargs, &block)
monitor = build_monitor(monitor_class, *args, **kwargs, &block)
Gitlab::Memory::Watchdog::MonitorState.new(monitor, max_strikes: max_strikes)
end
def build_monitor(monitor_class, *args, **kwargs, &block)
monitor_class.new(*args, **kwargs, &block)
end
end
DEFAULT_SLEEP_TIME_SECONDS = 60
attr_reader :monitors
attr_writer :logger, :handler, :sleep_time_seconds
def initialize
@monitors = MonitorStack.new
end
def handler
@handler ||= NullHandler.instance
end
def logger
@logger ||= Gitlab::Logger.new($stdout)
end
# Used to control the frequency with which the watchdog will wake up and poll the GC.
def sleep_time_seconds
@sleep_time_seconds ||= DEFAULT_SLEEP_TIME_SECONDS
end
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Gitlab
module Memory
class Watchdog
module Monitor
# A monitor that observes Ruby heap fragmentation and calls
# memory_violation_callback when the Ruby heap has been fragmented for an extended
# period of time.
#
# See Gitlab::Metrics::Memory for how heap fragmentation is defined.
class HeapFragmentation
attr_reader :max_heap_fragmentation
# max_heap_fragmentation:
# The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
def initialize(max_heap_fragmentation:)
@max_heap_fragmentation = max_heap_fragmentation
init_frag_limit_metrics
end
def call
heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
return { threshold_violated: false, payload: {} } unless heap_fragmentation > max_heap_fragmentation
{ threshold_violated: true, payload: payload(heap_fragmentation) }
end
private
def payload(heap_fragmentation)
{
message: 'heap fragmentation limit exceeded',
memwd_cur_heap_frag: heap_fragmentation,
memwd_max_heap_frag: max_heap_fragmentation
}
end
def init_frag_limit_metrics
heap_frag_limit = Gitlab::Metrics.gauge(
:gitlab_memwd_heap_frag_limit,
'The configured limit for how fragmented the Ruby heap is allowed to be'
)
heap_frag_limit.set({}, max_heap_fragmentation)
end
end
end
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module Memory
class Watchdog
module Monitor
class UniqueMemoryGrowth
attr_reader :max_mem_growth
def initialize(max_mem_growth:)
@max_mem_growth = max_mem_growth
end
def call
worker_uss = Gitlab::Metrics::System.memory_usage_uss_pss[:uss]
reference_uss = reference_mem[:uss]
memory_limit = max_mem_growth * reference_uss
return { threshold_violated: false, payload: {} } unless worker_uss > memory_limit
{ threshold_violated: true, payload: payload(worker_uss, reference_uss, memory_limit) }
end
private
def payload(worker_uss, reference_uss, memory_limit)
{
message: 'memory limit exceeded',
memwd_uss_bytes: worker_uss,
memwd_ref_uss_bytes: reference_uss,
memwd_max_uss_bytes: memory_limit
}
end
# On pre-fork systems this would be the primary process memory from which workers fork.
# Otherwise it is the current process' memory.
#
# We initialize this lazily because in the initializer the application may not have
# finished booting yet, which would yield an incorrect baseline.
def reference_mem
@reference_mem ||= Gitlab::Metrics::System.memory_usage_uss_pss(pid: Gitlab::Cluster::PRIMARY_PID)
end
end
end
end
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
module Gitlab
module Memory
class Watchdog
class MonitorState
class Result
attr_reader :payload
def initialize(strikes_exceeded:, threshold_violated:, monitor_class:, payload: )
@strikes_exceeded = strikes_exceeded
@threshold_violated = threshold_violated
@monitor_class = monitor_class
@payload = payload
end
def strikes_exceeded?
@strikes_exceeded
end
def threshold_violated?
@threshold_violated
end
def monitor_name
@monitor_class.name.demodulize.underscore.to_sym
end
end
def initialize(monitor, max_strikes:)
@monitor = monitor
@max_strikes = max_strikes
@strikes = 0
end
def call
reset_strikes if strikes_exceeded?
monitor_result = @monitor.call
if monitor_result[:threshold_violated]
issue_strike
else
reset_strikes
end
build_result(monitor_result)
end
def monitor_class
@monitor.class
end
private
def build_result(monitor_result)
Result.new(
strikes_exceeded: strikes_exceeded?,
monitor_class: monitor_class,
threshold_violated: monitor_result[:threshold_violated],
payload: payload.merge(monitor_result[:payload]))
end
def payload
{
memwd_max_strikes: @max_strikes,
memwd_cur_strikes: @strikes
}
end
def strikes_exceeded?
@strikes > @max_strikes
end
def issue_strike
@strikes += 1
end
def reset_strikes
@strikes = 0
end
end
end
end
end

View File

@ -15820,6 +15820,57 @@ msgstr ""
msgid "Events API"
msgstr ""
msgid "Event|accepted"
msgstr ""
msgid "Event|added"
msgstr ""
msgid "Event|approved"
msgstr ""
msgid "Event|closed"
msgstr ""
msgid "Event|commented on"
msgstr ""
msgid "Event|created"
msgstr ""
msgid "Event|deleted"
msgstr ""
msgid "Event|destroyed"
msgstr ""
msgid "Event|imported"
msgstr ""
msgid "Event|joined"
msgstr ""
msgid "Event|left"
msgstr ""
msgid "Event|opened"
msgstr ""
msgid "Event|pushed new"
msgstr ""
msgid "Event|pushed to"
msgstr ""
msgid "Event|removed"
msgstr ""
msgid "Event|removed due to membership expiration from"
msgstr ""
msgid "Event|updated"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
@ -16273,9 +16324,6 @@ msgstr ""
msgid "Failed to create import label for jira import."
msgstr ""
msgid "Failed to create new access token: %{token_response_message}"
msgstr ""
msgid "Failed to create repository"
msgstr ""
@ -18677,6 +18725,9 @@ msgstr ""
msgid "Group"
msgstr ""
msgid "Group %{group_name} and its Mattermost team were successfully created."
msgstr ""
msgid "Group %{group_name} couldn't be exported."
msgstr ""
@ -35312,10 +35363,10 @@ msgstr ""
msgid "Saving project."
msgstr ""
msgid "ScanExecutionPolicy|%{ifLabelStart}if%{ifLabelEnd} %{rules} actions for the %{scopes} %{branches}"
msgid "ScanExecutionPolicy|%{period} %{days} at %{time}"
msgstr ""
msgid "ScanExecutionPolicy|%{period} %{days} at %{time}"
msgid "ScanExecutionPolicy|%{rules} actions for the %{scopes} %{branches} %{agents} %{namespaces}"
msgstr ""
msgid "ScanExecutionPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require a %{scan} scan to run"
@ -35336,9 +35387,15 @@ msgstr ""
msgid "ScanExecutionPolicy|Schedule rule component"
msgstr ""
msgid "ScanExecutionPolicy|Select agent"
msgstr ""
msgid "ScanExecutionPolicy|Select branches"
msgstr ""
msgid "ScanExecutionPolicy|Select namespaces"
msgstr ""
msgid "ScanExecutionPolicy|Select scanner profile"
msgstr ""
@ -35348,9 +35405,15 @@ msgstr ""
msgid "ScanExecutionPolicy|Site profile"
msgstr ""
msgid "ScanExecutionPolicy|agent"
msgstr ""
msgid "ScanExecutionPolicy|branch"
msgstr ""
msgid "ScanExecutionPolicy|in namespaces"
msgstr ""
msgid "ScanResultPolicy|%{ifLabelStart}if%{ifLabelEnd} %{scanners} find(s) more than %{vulnerabilitiesAllowed} %{severities} %{vulnerabilityStates} vulnerabilities in an open merge request targeting %{branches}"
msgstr ""
@ -46784,9 +46847,6 @@ msgstr ""
msgid "Your new %{type}"
msgstr ""
msgid "Your new access token has been created."
msgstr ""
msgid "Your new comment"
msgstr ""
@ -46956,9 +47016,6 @@ msgstr[1] ""
msgid "access:"
msgstr ""
msgid "added"
msgstr ""
msgid "added %{emails}"
msgstr ""
@ -48625,9 +48682,6 @@ msgstr ""
msgid "remove weight"
msgstr ""
msgid "removed"
msgstr ""
msgid "removed a %{link_type} link"
msgstr ""

View File

@ -6,7 +6,7 @@ gem 'gitlab-qa', '~> 8', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.16.0'
gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23'
gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13'
gem 'rspec', '~> 3.10'
gem 'selenium-webdriver', '~> 4.0'
@ -21,7 +21,7 @@ gem 'rotp', '~> 3.1.0'
gem 'timecop', '~> 0.9.5'
gem 'parallel', '~> 1.19'
gem 'rainbow', '~> 3.0.0'
gem 'rspec-parameterized', '~> 0.4.2'
gem 'rspec-parameterized', '~> 0.5.2'
gem 'octokit', '~> 5.6.1'
gem "faraday-retry", "~> 2.0"
gem 'webdrivers', '~> 5.2'

View File

@ -1,16 +1,12 @@
GEM
remote: https://rubygems.org/
specs:
abstract_type (0.0.7)
activesupport (6.1.4.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
airborne (0.3.4)
@ -39,7 +35,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-screenshot (1.0.23)
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
chemlab (0.9.2)
@ -53,9 +49,6 @@ GEM
childprocess (4.1.0)
coderay (1.1.2)
colorize (0.8.1)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.1.10)
confiner (0.3.0)
gitlab (>= 4.17)
@ -66,7 +59,6 @@ GEM
diff-lcs (1.3)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
equalizer (0.0.11)
excon (0.92.4)
faker (2.19.0)
i18n (>= 1.6, < 2)
@ -162,21 +154,18 @@ GEM
httpclient (2.8.3)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb-client (1.17.0)
jwt (2.5.0)
knapsack (4.0.0)
rake
launchy (2.4.3)
addressable (~> 2.3)
launchy (2.5.0)
addressable (~> 2.7)
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
macaddr (1.7.2)
systemu (~> 2.6.5)
memoist (0.16.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
@ -204,7 +193,6 @@ GEM
coderay
parser
unparser
procto (0.0.3)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@ -244,7 +232,7 @@ GEM
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-parameterized (0.4.2)
rspec-parameterized (0.5.2)
binding_ninja (>= 0.2.3)
parser
proc_to_ast
@ -276,7 +264,6 @@ GEM
table_print (1.5.7)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thread_safe (0.3.6)
timecop (0.9.5)
trailblazer-option (0.1.2)
tzinfo (2.0.5)
@ -286,14 +273,9 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.3.0)
unparser (0.4.7)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
unparser (0.6.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.6.5)
procto (~> 0.0.2)
parser (>= 3.1.0)
uuid (2.3.9)
macaddr (~> 1.0)
warning (1.3.0)
@ -317,7 +299,7 @@ DEPENDENCIES
airborne (~> 0.3.4)
allure-rspec (~> 2.16.0)
capybara (~> 3.35.0)
capybara-screenshot (~> 1.0.23)
capybara-screenshot (~> 1.0.26)
chemlab (~> 0.9)
chemlab-library-www-gitlab-com (~> 0.1)
confiner (~> 0.3)
@ -339,7 +321,7 @@ DEPENDENCIES
rest-client (~> 2.1.0)
rotp (~> 3.1.0)
rspec (~> 3.10)
rspec-parameterized (~> 0.4.2)
rspec-parameterized (~> 0.5.2)
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
ruby-debug-ide (~> 0.7.3)

View File

@ -0,0 +1,117 @@
# frozen_string_literal: true
require 'open3'
class MigrationSchemaValidator
FILENAME = 'db/structure.sql'
MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
SCHEMA_VERSION_DIR = 'db/schema_migrations'
VERSION_DIGITS = 14
def validate!
if committed_migrations.empty?
puts "\e[32m No migrations found, skipping schema validation\e[0m"
return
end
validate_schema_on_rollback!
validate_schema_on_migrate!
validate_schema_version_files!
end
private
def validate_schema_on_rollback!
committed_migrations.reverse_each do |filename|
version = find_migration_version(filename)
run("scripts/db_tasks db:migrate:down VERSION=#{version}")
run("scripts/db_tasks db:schema:dump")
end
git_command = "git diff #{diff_target} -- #{FILENAME}"
base_message = "rollback of added migrations does not revert #{FILENAME} to previous state"
validate_clean_output!(git_command, base_message)
end
def validate_schema_on_migrate!
run("scripts/db_tasks db:migrate")
run("scripts/db_tasks db:schema:dump")
git_command = "git diff -- #{FILENAME}"
base_message = "the committed #{FILENAME} does not match the one generated by running added migrations"
validate_clean_output!(git_command, base_message)
end
def validate_schema_version_files!
git_command = "git add -A -n #{SCHEMA_VERSION_DIR}"
base_message = "the committed files in #{SCHEMA_VERSION_DIR} do not match those expected by the added migrations"
validate_clean_output!(git_command, base_message)
end
def committed_migrations
@committed_migrations ||= begin
git_command = "git diff --name-only --diff-filter=A #{diff_target} -- #{MIGRATION_DIRS.join(' ')}"
run(git_command).split("\n")
end
end
def diff_target
@diff_target ||= pipeline_for_merged_results? ? target_branch : merge_base
end
def merge_base
run("git merge-base #{target_branch} #{source_ref}")
end
def target_branch
ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || ENV['CI_DEFAULT_BRANCH'] || 'master'
end
def source_ref
ENV['CI_COMMIT_SHA'] || 'HEAD'
end
def pipeline_for_merged_results?
ENV.key?('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA')
end
def find_migration_version(filename)
file_basename = File.basename(filename)
version_match = /\A(?<version>\d{#{VERSION_DIGITS}})_/o.match(file_basename)
die "#{filename} has an invalid migration version" if version_match.nil?
version_match[:version]
end
def validate_clean_output!(command, base_message)
command_output = run(command)
return if command_output.empty?
die "#{base_message}:\n#{command_output}"
end
def die(message, error_code: 1)
puts "\e[31mError: #{message}\e[0m"
exit error_code
end
def run(cmd)
puts "\e[32m$ #{cmd}\e[37m"
stdout_str, stderr_str, status = Open3.capture3(cmd)
puts "#{stdout_str}#{stderr_str}\e[0m"
die "command failed: #{stderr_str}" unless status.success?
stdout_str.chomp
end
end

View File

@ -96,7 +96,7 @@ def timed(task)
puts "#{task} finished in #{Time.now - start} seconds.\n"
end
if $0 == __FILE__
if $PROGRAM_NAME == __FILE__
options = {
dry_run: false
}

View File

@ -0,0 +1,31 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'migration_schema_validator'
class PostDeploymentMigrationsValidator < MigrationSchemaValidator
def validate!
if committed_migrations.empty?
puts "\e[32m No migrations found, skipping post-deployment migrations validation\e[0m"
return
end
rollback_commited_migrations
run("SKIP_POST_DEPLOYMENT_MIGRATIONS=true scripts/db_tasks db:migrate")
run("scripts/db_tasks db:migrate")
end
private
def rollback_commited_migrations
committed_migrations.reverse_each do |filename|
version = find_migration_version(filename)
run("scripts/db_tasks db:migrate:down VERSION=#{version}")
end
end
end
PostDeploymentMigrationsValidator.new.validate!

View File

@ -265,7 +265,7 @@ def timed(task)
puts "#{task} finished in #{Time.now - start} seconds.\n"
end
if $0 == __FILE__
if $PROGRAM_NAME == __FILE__
options = {
dry_run: false
}

View File

@ -2,120 +2,6 @@
# frozen_string_literal: true
require 'open3'
class MigrationSchemaValidator
FILENAME = 'db/structure.sql'
MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
SCHEMA_VERSION_DIR = 'db/schema_migrations'
VERSION_DIGITS = 14
def validate!
if committed_migrations.empty?
puts "\e[32m No migrations found, skipping schema validation\e[0m"
return
end
validate_schema_on_rollback!
validate_schema_on_migrate!
validate_schema_version_files!
end
private
def validate_schema_on_rollback!
committed_migrations.reverse_each do |filename|
version = find_migration_version(filename)
run("scripts/db_tasks db:migrate:down VERSION=#{version}")
run("scripts/db_tasks db:schema:dump")
end
git_command = "git diff #{diff_target} -- #{FILENAME}"
base_message = "rollback of added migrations does not revert #{FILENAME} to previous state"
validate_clean_output!(git_command, base_message)
end
def validate_schema_on_migrate!
run("scripts/db_tasks db:migrate")
run("scripts/db_tasks db:schema:dump")
git_command = "git diff -- #{FILENAME}"
base_message = "the committed #{FILENAME} does not match the one generated by running added migrations"
validate_clean_output!(git_command, base_message)
end
def validate_schema_version_files!
git_command = "git add -A -n #{SCHEMA_VERSION_DIR}"
base_message = "the committed files in #{SCHEMA_VERSION_DIR} do not match those expected by the added migrations"
validate_clean_output!(git_command, base_message)
end
def committed_migrations
@committed_migrations ||= begin
git_command = "git diff --name-only --diff-filter=A #{diff_target} -- #{MIGRATION_DIRS.join(' ')}"
run(git_command).split("\n")
end
end
def diff_target
@diff_target ||= pipeline_for_merged_results? ? target_branch : merge_base
end
def merge_base
run("git merge-base #{target_branch} #{source_ref}")
end
def target_branch
ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || ENV['CI_DEFAULT_BRANCH'] || 'master'
end
def source_ref
ENV['CI_COMMIT_SHA'] || 'HEAD'
end
def pipeline_for_merged_results?
ENV.key?('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA')
end
def find_migration_version(filename)
file_basename = File.basename(filename)
version_match = /\A(?<version>\d{#{VERSION_DIGITS}})_/o.match(file_basename)
die "#{filename} has an invalid migration version" if version_match.nil?
version_match[:version]
end
def validate_clean_output!(command, base_message)
command_output = run(command)
return if command_output.empty?
die "#{base_message}:\n#{command_output}"
end
def die(message, error_code: 1)
puts "\e[31mError: #{message}\e[0m"
exit error_code
end
def run(cmd)
puts "\e[32m$ #{cmd}\e[37m"
stdout_str, stderr_str, status = Open3.capture3(cmd)
puts "#{stdout_str}#{stderr_str}\e[0m"
die "command failed: #{stderr_str}" unless status.success?
stdout_str.chomp
end
end
require_relative 'migration_schema_validator'
MigrationSchemaValidator.new.validate!

View File

@ -72,29 +72,11 @@ RSpec.describe Groups::RunnersController do
expect(response).to render_template(:show)
end
context 'when runners_finder_all_available is enabled' do
before do
stub_feature_flags(runners_finder_all_available: true)
end
it 'renders show with 200 status code instance runner' do
get :show, params: { group_id: group, id: instance_runner }
it 'renders show with 200 status code instance runner' do
get :show, params: { group_id: group, id: instance_runner }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
context 'when runners_finder_all_available is disabled' do
before do
stub_feature_flags(runners_finder_all_available: false)
end
it 'renders show with a 404 instance runner' do
get :show, params: { group_id: group, id: instance_runner }
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
it 'renders show with 200 status code project runner' do

View File

@ -229,7 +229,7 @@ RSpec.describe GroupsController, factory_default: :keep do
sign_in(user)
expect do
post :create, params: { group: { name: 'new_group', path: "new_group" } }
post :create, params: { group: { name: 'new_group', path: 'new_group' } }
end.to change { Group.count }.by(1)
expect(response).to have_gitlab_http_status(:found)
@ -240,13 +240,31 @@ RSpec.describe GroupsController, factory_default: :keep do
sign_in(create(:admin))
expect do
post :create, params: { group: { name: 'new_group', path: "new_group" } }
post :create, params: { group: { name: 'new_group', path: 'new_group' } }
end.to change { Group.count }.by(1)
expect(response).to have_gitlab_http_status(:found)
end
end
context 'when creating chat team' do
before do
stub_mattermost_setting(enabled: true)
end
it 'triggers Mattermost::CreateTeamService' do
sign_in(user)
expect_next_instance_of(::Mattermost::CreateTeamService) do |service|
expect(service).to receive(:execute).and_return({ name: 'test-chat-team', id: 1 })
end
post :create, params: { group: { name: 'new_group', path: 'new_group', create_chat_team: 1 } }
expect(response).to have_gitlab_http_status(:found)
end
end
context 'when creating subgroups' do
[true, false].each do |can_create_group_status|
context "and can_create_group is #{can_create_group_status}" do

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :namespace_callout, class: 'Users::NamespaceCallout' do
feature_name { :invite_members_banner }
user
namespace
end
end

View File

@ -119,45 +119,27 @@ RSpec.describe "Group Runners" do
create(:ci_runner, :instance, description: 'runner-baz', contacted_at: Time.zone.now)
end
context "when runners_finder_all_available is enabled" do
before do
stub_feature_flags(runners_finder_all_available: true)
visit group_runners_path(group)
end
context "when selecting 'Show only inherited'" do
before do
find("[data-testid='runner-membership-toggle'] button").click
wait_for_requests
end
it_behaves_like 'shows runner in list' do
let(:runner) { instance_runner }
end
it 'shows runner details page' do
click_link("##{instance_runner.id} (#{instance_runner.short_sha})")
expect(current_url).to include(group_runner_path(group, instance_runner))
expect(page).to have_content "#{s_('Runners|Description')} runner-baz"
end
end
before do
visit group_runners_path(group)
end
context "when runners_finder_all_available is disabled" do
context "when selecting 'Show only inherited'" do
before do
stub_feature_flags(runners_finder_all_available: false)
find("[data-testid='runner-membership-toggle'] button").click
visit group_runners_path(group)
wait_for_requests
end
it "does not display 'Show only inherited' toggle" do
expect(page).not_to have_content(s_('Runners|Show only inherited'))
it_behaves_like 'shows runner in list' do
let(:runner) { instance_runner }
end
it_behaves_like 'shows no runners registered'
it 'shows runner details page' do
click_link("##{instance_runner.id} (#{instance_runner.short_sha})")
expect(current_url).to include(group_runner_path(group, instance_runner))
expect(page).to have_content "#{s_('Runners|Description')} runner-baz"
end
end
end

View File

@ -322,27 +322,11 @@ RSpec.describe Ci::RunnersFinder do
context 'with :all_available membership' do
let(:membership) { :all_available }
context 'with runners_finder_all_available FF disabled' do
before do
stub_feature_flags(runners_finder_all_available: false)
end
it 'returns no runners' do
expect(subject).to be_empty
end
end
context 'with runners_finder_all_available FF enabled' do
before do
stub_feature_flags(runners_finder_all_available: [target_group])
end
it 'returns runners available to group' do
expect(subject).to match_array([runner_project_7, runner_project_6, runner_project_5,
runner_project_4, runner_project_3, runner_project_2,
runner_project_1, runner_sub_group_4, runner_sub_group_3,
runner_sub_group_2, runner_sub_group_1, runner_group, runner_instance])
end
it 'returns runners available to group' do
expect(subject).to match_array([runner_project_7, runner_project_6, runner_project_5,
runner_project_4, runner_project_3, runner_project_2,
runner_project_1, runner_sub_group_4, runner_sub_group_3,
runner_sub_group_2, runner_sub_group_1, runner_group, runner_instance])
end
end
@ -448,14 +432,8 @@ RSpec.describe Ci::RunnersFinder do
context 'with :all_available membership' do
let(:membership) { :all_available }
context 'with runners_finder_all_available FF enabled' do
before do
stub_feature_flags(runners_finder_all_available: [target_group])
end
it 'returns no runners' do
expect(subject).to be_empty
end
it 'returns no runners' do
expect(subject).to be_empty
end
end
end

View File

@ -22,6 +22,8 @@ describe('~/access_tokens/components/new_access_token_app', () => {
});
};
const findButtonEl = () => document.querySelector('[type=submit]');
const triggerSuccess = async (newToken = 'new token') => {
wrapper
.findComponent(DomElementListener)
@ -41,7 +43,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
<input type="text" id="expires_at" value="2022-01-01"/>
<input type="text" value='1'/>
<input type="checkbox" checked/>
<input type="submit" value="Create"/>
<button type="submit" value="Create" class="disabled" disabled="disabled"/>
</form>`,
);
@ -120,10 +122,10 @@ describe('~/access_tokens/components/new_access_token_app', () => {
});
it('should not reset the submit button value', async () => {
expect(document.querySelector('input[type=submit]').value).toBe('Create');
expect(findButtonEl().value).toBe('Create');
await triggerSuccess();
expect(document.querySelector('input[type=submit]').value).toBe('Create');
expect(findButtonEl().value).toBe('Create');
});
});
});
@ -162,6 +164,17 @@ describe('~/access_tokens/components/new_access_token_app', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
it('should enable the submit button', async () => {
const button = findButtonEl();
expect(button).toBeDisabled();
expect(button.className).toBe('disabled');
await triggerError();
expect(button).not.toBeDisabled();
expect(button.className).toBe('');
});
});
describe('before error or success', () => {

View File

@ -159,55 +159,30 @@ describe('GroupRunnersApp', () => {
});
describe('show all available runners toggle', () => {
describe('when runners_finder_all_available is enabled', () => {
it('shows the membership toggle', () => {
createComponent({
provide: {
glFeatures: { runnersFinderAllAvailable: true },
},
});
expect(findRunnerMembershipToggle().exists()).toBe(true);
});
it('sets the membership toggle', () => {
setWindowLocation(`?membership[]=${MEMBERSHIP_ALL_AVAILABLE}`);
createComponent({
provide: {
glFeatures: { runnersFinderAllAvailable: true },
},
});
expect(findRunnerMembershipToggle().props('value')).toBe(MEMBERSHIP_ALL_AVAILABLE);
});
it('requests filter', async () => {
createComponent({
provide: {
glFeatures: { runnersFinderAllAvailable: true },
},
});
findRunnerMembershipToggle().vm.$emit('input', MEMBERSHIP_ALL_AVAILABLE);
await waitForPromises();
expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
membership: MEMBERSHIP_ALL_AVAILABLE,
}),
);
});
it('shows the membership toggle', () => {
createComponent();
expect(findRunnerMembershipToggle().exists()).toBe(true);
});
describe('when runners_finder_all_available is disabled', () => {
beforeEach(() => {
createComponent();
});
it('sets the membership toggle', () => {
setWindowLocation(`?membership[]=${MEMBERSHIP_ALL_AVAILABLE}`);
it('does not show the membership toggle', () => {
expect(findRunnerMembershipToggle().exists()).toBe(false);
});
createComponent();
expect(findRunnerMembershipToggle().props('value')).toBe(MEMBERSHIP_ALL_AVAILABLE);
});
it('requests filter', async () => {
createComponent();
findRunnerMembershipToggle().vm.$emit('input', MEMBERSHIP_ALL_AVAILABLE);
await waitForPromises();
expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
membership: MEMBERSHIP_ALL_AVAILABLE,
}),
);
});
});

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import TokenAccess from '~/token_access/components/token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
@ -144,7 +144,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
});
@ -187,7 +187,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
});
});

View File

@ -1,7 +1,7 @@
import { nextTick } from 'vue';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
@ -129,7 +129,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error', () => {
expect(createFlash).toHaveBeenCalledWith({ message: FETCH_ERROR });
expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR });
});
});
@ -268,7 +268,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
expect(createFlash).toHaveBeenCalledWith({ message: APPROVE_ERROR });
expect(createAlert).toHaveBeenCalledWith({ message: APPROVE_ERROR });
});
});
});
@ -319,7 +319,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
expect(createFlash).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});
});

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { createAlert } from '~/flash';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import {
@ -168,7 +168,7 @@ describe('DeploymentAction component', () => {
});
it('should not throw an error', () => {
expect(createFlash).not.toHaveBeenCalled();
expect(createAlert).not.toHaveBeenCalled();
});
describe('response includes redirect_url', () => {
@ -225,9 +225,8 @@ describe('DeploymentAction component', () => {
finderFn().trigger('click');
});
it('should call createFlash with error message', () => {
expect(createFlash).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
it('should call createAlert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: actionButtonMocks[configConst].errorMessage,
});
});

View File

@ -26,8 +26,29 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update do
RSpec.shared_examples 'updating the namespace package setting' do
it_behaves_like 'updating the namespace package setting attributes',
from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT', generic_duplicates_allowed: true, generic_duplicate_exception_regex: 'foo' },
to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE', generic_duplicates_allowed: false, generic_duplicate_exception_regex: 'bar' }
from: {
maven_duplicates_allowed: true,
maven_duplicate_exception_regex: 'SNAPSHOT',
generic_duplicates_allowed: true,
generic_duplicate_exception_regex: 'foo',
maven_package_requests_forwarding: nil,
lock_maven_package_requests_forwarding: false,
npm_package_requests_forwarding: nil,
lock_npm_package_requests_forwarding: false,
pypi_package_requests_forwarding: nil,
lock_pypi_package_requests_forwarding: false
}, to: {
maven_duplicates_allowed: false,
maven_duplicate_exception_regex: 'RELEASE',
generic_duplicates_allowed: false,
generic_duplicate_exception_regex: 'bar',
maven_package_requests_forwarding: true,
lock_maven_package_requests_forwarding: true,
npm_package_requests_forwarding: true,
lock_npm_package_requests_forwarding: true,
pypi_package_requests_forwarding: true,
lock_pypi_package_requests_forwarding: true
}
it_behaves_like 'returning a success'
@ -59,11 +80,19 @@ RSpec.describe Mutations::Namespace::PackageSettings::Update do
context 'with existing namespace package setting' do
let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) }
let_it_be(:params) do
{ namespace_path: namespace.full_path,
{
namespace_path: namespace.full_path,
maven_duplicates_allowed: false,
maven_duplicate_exception_regex: 'RELEASE',
generic_duplicates_allowed: false,
generic_duplicate_exception_regex: 'bar' }
generic_duplicate_exception_regex: 'bar',
maven_package_requests_forwarding: true,
lock_maven_package_requests_forwarding: true,
npm_package_requests_forwarding: true,
lock_npm_package_requests_forwarding: true,
pypi_package_requests_forwarding: true,
lock_pypi_package_requests_forwarding: true
}
end
where(:user_role, :shared_examples_name) do

View File

@ -14,4 +14,24 @@ RSpec.describe GitlabSchema.types['PackageSettings'] do
it { is_expected.to have_graphql_type(Types::UntrustedRegexp) }
end
it 'includes package setting fields' do
expected_fields = %w[
maven_duplicates_allowed
maven_duplicate_exception_regex
generic_duplicates_allowed
generic_duplicate_exception_regex
maven_package_requests_forwarding
lock_maven_package_requests_forwarding
npm_package_requests_forwarding
lock_npm_package_requests_forwarding
pypi_package_requests_forwarding
lock_pypi_package_requests_forwarding
maven_package_requests_forwarding_locked
npm_package_requests_forwarding_locked
pypi_package_requests_forwarding_locked
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end

View File

@ -24,6 +24,45 @@ RSpec.describe EventsHelper do
end
end
describe '#localized_action_name' do
it 'handles all valid design events' do
created, updated, destroyed = %i[created updated destroyed].map do |trait|
event = build(:design_event, trait)
helper.localized_action_name(event)
end
expect(created).to eq(_('added'))
expect(updated).to eq(_('updated'))
expect(destroyed).to eq(_('removed'))
end
context 'handles correct base actions' do
using RSpec::Parameterized::TableSyntax
where(:trait, :localized_action_name) do
:created | s_('Event|created')
:updated | s_('Event|opened')
:closed | s_('Event|closed')
:reopened | s_('Event|opened')
:commented | s_('Event|commented on')
:merged | s_('Event|accepted')
:joined | s_('Event|joined')
:left | s_('Event|left')
:destroyed | s_('Event|destroyed')
:expired | s_('Event|removed due to membership expiration from')
:approved | s_('Event|approved')
end
with_them do
it 'with correct name and method' do
event = build(:event, trait)
expect(helper.localized_action_name(event)).to eq(localized_action_name)
end
end
end
end
describe '#event_commit_title' do
let(:message) { 'foo & bar ' + 'A' * 70 + '\n' + 'B' * 80 }

View File

@ -4,7 +4,7 @@ require 'fast_spec_helper'
RSpec.describe 'memory watchdog' do
subject(:run_initializer) do
load Rails.root.join('config/initializers/memory_watchdog.rb')
load rails_root_join('config/initializers/memory_watchdog.rb')
end
context 'when GITLAB_MEMORY_WATCHDOG_ENABLED is truthy' do
@ -17,6 +17,7 @@ RSpec.describe 'memory watchdog' do
context 'when runtime is an application' do
let(:watchdog) { instance_double(Gitlab::Memory::Watchdog) }
let(:background_task) { instance_double(Gitlab::BackgroundTask) }
let(:logger) { Gitlab::AppLogger }
before do
allow(Gitlab::Runtime).to receive(:application?).and_return(true)
@ -28,16 +29,65 @@ RSpec.describe 'memory watchdog' do
run_initializer
end
shared_examples 'starts watchdog with handler' do |handler_class|
it "uses the #{handler_class} and starts the watchdog" do
expect(Gitlab::Memory::Watchdog).to receive(:new).with(
handler: an_instance_of(handler_class),
logger: Gitlab::AppLogger).and_return(watchdog)
expect(Gitlab::BackgroundTask).to receive(:new).with(watchdog).and_return(background_task)
expect(background_task).to receive(:start)
expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_yield
shared_examples 'starts configured watchdog' do |handler_class|
let(:configuration) { Gitlab::Memory::Watchdog::Configuration.new }
let(:watchdog_monitors_params) do
{
Gitlab::Memory::Watchdog::Monitor::HeapFragmentation => {
max_heap_fragmentation: max_heap_fragmentation,
max_strikes: max_strikes
},
Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth => {
max_mem_growth: max_mem_growth,
max_strikes: max_strikes
}
}
end
run_initializer
shared_examples 'configures and starts watchdog' do
it "correctly configures and starts watchdog", :aggregate_failures do
expect(watchdog).to receive(:configure).and_yield(configuration)
watchdog_monitors_params.each do |monitor_class, params|
expect(configuration.monitors).to receive(:use).with(monitor_class, **params)
end
expect(Gitlab::Memory::Watchdog).to receive(:new).and_return(watchdog)
expect(Gitlab::BackgroundTask).to receive(:new).with(watchdog).and_return(background_task)
expect(background_task).to receive(:start)
expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_yield
run_initializer
expect(configuration.handler).to be_an_instance_of(handler_class)
expect(configuration.logger).to eq(logger)
expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds)
end
end
context 'when settings are not passed through the environment' do
let(:max_strikes) { 5 }
let(:max_heap_fragmentation) { 0.5 }
let(:max_mem_growth) { 3.0 }
let(:sleep_time_seconds) { 60 }
include_examples 'configures and starts watchdog'
end
context 'when settings are passed through the environment' do
let(:max_strikes) { 6 }
let(:max_heap_fragmentation) { 0.4 }
let(:max_mem_growth) { 2.0 }
let(:sleep_time_seconds) { 50 }
before do
stub_env('GITLAB_MEMWD_MAX_STRIKES', 6)
stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 50)
stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 2.0)
stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.4)
end
include_examples 'configures and starts watchdog'
end
end
@ -59,7 +109,7 @@ RSpec.describe 'memory watchdog' do
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
end
it_behaves_like 'starts watchdog with handler', Gitlab::Memory::Watchdog::PumaHandler
it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::PumaHandler
end
# rubocop: enable RSpec/VerifiedDoubles
@ -68,11 +118,11 @@ RSpec.describe 'memory watchdog' do
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
end
it_behaves_like 'starts watchdog with handler', Gitlab::Memory::Watchdog::TermProcessHandler
it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::TermProcessHandler
end
context 'when other runtime' do
it_behaves_like 'starts watchdog with handler', Gitlab::Memory::Watchdog::NullHandler
it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::NullHandler
end
end

View File

@ -58,16 +58,15 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
issues
end
let(:project_identifier) { 'namespace/repo' }
let(:data) { { 'token' => 'token' } }
let_it_be(:project_identifier) { 'namespace/repo' }
let(:project) do
let_it_be_with_reload(:project) do
create(
:project,
:repository,
import_source: project_identifier,
import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data }
import_data_attributes: { credentials: { 'token' => 'token' } }
)
end
@ -80,6 +79,14 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
}
end
let(:last_issue_data) do
{
page: 1,
pagelen: 1,
values: [sample_issues_statuses.last]
}
end
let(:counter) { double('counter', increment: true) }
subject { described_class.new(project) }
@ -243,6 +250,13 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
headers: { "Content-Type" => "application/json" },
body: { has_issues: true, full_name: project_identifier }.to_json)
stub_request(
:get,
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=1&sort=-created_on&state=ALL"
).to_return(status: 200,
headers: { "Content-Type" => "application/json" },
body: last_issue_data.to_json)
stub_request(
:get,
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on"
@ -344,6 +358,12 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
end
describe 'issue import' do
it 'allocates internal ids' do
expect(Issue).to receive(:track_project_iid!).with(project, 6)
importer.execute
end
it 'maps reporters to anonymous if bitbucket reporter is nil' do
allow(importer).to receive(:import_wiki)
importer.execute
@ -363,6 +383,29 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
expect(project.issues.map(&:work_item_type_id).uniq).to contain_exactly(WorkItems::Type.default_issue_type.id)
end
context 'with issue comments' do
let(:inline_note) do
instance_double(Bitbucket::Representation::Comment, note: 'Hello world', author: 'someuser', created_at: Time.now, updated_at: Time.now)
end
before do
allow_next_instance_of(Bitbucket::Client) do |instance|
allow(instance).to receive(:issue_comments).and_return([inline_note])
end
end
it 'imports issue comments' do
allow(importer).to receive(:import_wiki)
importer.execute
comment = project.notes.first
expect(project.notes.size).to eq(7)
expect(comment.note).to include(inline_note.note)
expect(comment.note).to include(inline_note.author)
expect(importer.errors).to be_empty
end
end
end
context 'metrics' do

Some files were not shown because too many files have changed in this diff Show More