Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
589ee0e419
commit
8bf2e2b73e
61 changed files with 604 additions and 348 deletions
28
app/assets/javascripts/issuable/bulk_update_sidebar/index.js
Normal file
28
app/assets/javascripts/issuable/bulk_update_sidebar/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import StatusSelect from './components/status_select.vue';
|
||||||
|
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||||
|
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||||
|
|
||||||
|
export function initBulkUpdateSidebar(prefixId) {
|
||||||
|
const el = document.querySelector('.issues-bulk-update');
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
issuableBulkUpdateActions.init({ prefixId });
|
||||||
|
new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initIssueStatusSelect() {
|
||||||
|
const el = document.querySelector('.js-issue-status');
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
render: (createElement) => createElement(StatusSelect),
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
import Vue from 'vue';
|
|
||||||
import StatusSelect from './components/status_select.vue';
|
|
||||||
|
|
||||||
export default function initIssueStatusSelect() {
|
|
||||||
const el = document.querySelector('.js-issue-status');
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Vue({
|
|
||||||
el,
|
|
||||||
render(h) {
|
|
||||||
return h(StatusSelect);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import { property } from 'lodash';
|
||||||
import issuableEventHub from '~/issues_list/eventhub';
|
import issuableEventHub from '~/issues_list/eventhub';
|
||||||
import LabelsSelect from '~/labels/labels_select';
|
import LabelsSelect from '~/labels/labels_select';
|
||||||
import MilestoneSelect from '~/milestones/milestone_select';
|
import MilestoneSelect from '~/milestones/milestone_select';
|
||||||
import initIssueStatusSelect from './init_issue_status_select';
|
|
||||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||||
import subscriptionSelect from './subscription_select';
|
import subscriptionSelect from './subscription_select';
|
||||||
|
|
||||||
|
@ -57,7 +56,6 @@ export default class IssuableBulkUpdateSidebar {
|
||||||
initDropdowns() {
|
initDropdowns() {
|
||||||
new LabelsSelect();
|
new LabelsSelect();
|
||||||
new MilestoneSelect();
|
new MilestoneSelect();
|
||||||
initIssueStatusSelect();
|
|
||||||
subscriptionSelect();
|
subscriptionSelect();
|
||||||
|
|
||||||
if (IS_EE) {
|
if (IS_EE) {
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
|
|
||||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
bulkUpdateSidebar: null,
|
|
||||||
|
|
||||||
init(prefixId) {
|
|
||||||
const bulkUpdateEl = document.querySelector('.issues-bulk-update');
|
|
||||||
const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
|
|
||||||
|
|
||||||
if (bulkUpdateEl && !alreadyInitialized) {
|
|
||||||
issuableBulkUpdateActions.init({ prefixId });
|
|
||||||
|
|
||||||
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.bulkUpdateSidebar;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -11,7 +11,9 @@ import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
|
||||||
export function initCsvImportExportButtons() {
|
export function initCsvImportExportButtons() {
|
||||||
const el = document.querySelector('.js-csv-import-export-buttons');
|
const el = document.querySelector('.js-csv-import-export-buttons');
|
||||||
|
|
||||||
if (!el) return null;
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showExportButton,
|
showExportButton,
|
||||||
|
@ -42,23 +44,24 @@ export function initCsvImportExportButtons() {
|
||||||
maxAttachmentSize,
|
maxAttachmentSize,
|
||||||
showLabel,
|
showLabel,
|
||||||
},
|
},
|
||||||
render(h) {
|
render: (createElement) =>
|
||||||
return h(CsvImportExportButtons, {
|
createElement(CsvImportExportButtons, {
|
||||||
props: {
|
props: {
|
||||||
exportCsvPath,
|
exportCsvPath,
|
||||||
issuableCount: parseInt(issuableCount, 10),
|
issuableCount: parseInt(issuableCount, 10),
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initIssuableByEmail() {
|
export function initIssuableByEmail() {
|
||||||
Vue.use(GlToast);
|
|
||||||
|
|
||||||
const el = document.querySelector('.js-issuable-by-email');
|
const el = document.querySelector('.js-issuable-by-email');
|
||||||
|
|
||||||
if (!el) return null;
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.use(GlToast);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initialEmail,
|
initialEmail,
|
||||||
|
@ -79,9 +82,7 @@ export function initIssuableByEmail() {
|
||||||
markdownHelpPath,
|
markdownHelpPath,
|
||||||
resetPath,
|
resetPath,
|
||||||
},
|
},
|
||||||
render(h) {
|
render: (createElement) => createElement(IssuableByEmail),
|
||||||
return h(IssuableByEmail);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ export function initIssuableHeaderWarnings(store) {
|
||||||
const el = document.getElementById('js-issuable-header-warnings');
|
const el = document.getElementById('js-issuable-header-warnings');
|
||||||
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hidden } = el.dataset;
|
const { hidden } = el.dataset;
|
||||||
|
@ -98,18 +99,18 @@ export function initIssuableHeaderWarnings(store) {
|
||||||
el,
|
el,
|
||||||
store,
|
store,
|
||||||
provide: { hidden: parseBoolean(hidden) },
|
provide: { hidden: parseBoolean(hidden) },
|
||||||
render(createElement) {
|
render: (createElement) => createElement(IssuableHeaderWarnings),
|
||||||
return createElement(IssuableHeaderWarnings);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initIssuableSidebar() {
|
export function initIssuableSidebar() {
|
||||||
const sidebarOptEl = document.querySelector('.js-sidebar-options');
|
const el = document.querySelector('.js-sidebar-options');
|
||||||
|
|
||||||
if (!sidebarOptEl) return;
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sidebarOptions = getSidebarOptions(sidebarOptEl);
|
const sidebarOptions = getSidebarOptions(el);
|
||||||
|
|
||||||
new IssuableContext(sidebarOptions.currentUser); // eslint-disable-line no-new
|
new IssuableContext(sidebarOptions.currentUser); // eslint-disable-line no-new
|
||||||
Sidebar.initialize();
|
Sidebar.initialize();
|
||||||
|
|
|
@ -517,10 +517,9 @@ export default {
|
||||||
},
|
},
|
||||||
async handleBulkUpdateClick() {
|
async handleBulkUpdateClick() {
|
||||||
if (!this.hasInitBulkEdit) {
|
if (!this.hasInitBulkEdit) {
|
||||||
const initBulkUpdateSidebar = await import(
|
const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
|
||||||
'~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar'
|
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
|
||||||
);
|
bulkUpdateSidebar.initIssueStatusSelect();
|
||||||
initBulkUpdateSidebar.default.init('issuable_');
|
|
||||||
|
|
||||||
const usersSelect = await import('~/users_select');
|
const usersSelect = await import('~/users_select');
|
||||||
const UsersSelect = usersSelect.default;
|
const UsersSelect = usersSelect.default;
|
||||||
|
|
|
@ -172,8 +172,12 @@ export default class Todos {
|
||||||
|
|
||||||
updateBadges(data) {
|
updateBadges(data) {
|
||||||
$(document).trigger('todo:toggle', data.count);
|
$(document).trigger('todo:toggle', data.count);
|
||||||
document.querySelector('.js-todos-pending .badge').innerHTML = addDelimiter(data.count);
|
document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
|
||||||
document.querySelector('.js-todos-done .badge').innerHTML = addDelimiter(data.done_count);
|
data.count,
|
||||||
|
);
|
||||||
|
document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
|
||||||
|
data.done_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
goToTodoUrl(e) {
|
goToTodoUrl(e) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
|
||||||
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
|
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
|
||||||
import initManualOrdering from '~/issues/manual_ordering';
|
import initManualOrdering from '~/issues/manual_ordering';
|
||||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||||
|
@ -13,7 +13,7 @@ if (gon.features?.vueIssuesList) {
|
||||||
|
|
||||||
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
|
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
|
||||||
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
|
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
|
||||||
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
|
initBulkUpdateSidebar(ISSUE_BULK_UPDATE_PREFIX);
|
||||||
|
|
||||||
initFilteredSearch({
|
initFilteredSearch({
|
||||||
page: FILTERED_SEARCH.ISSUES,
|
page: FILTERED_SEARCH.ISSUES,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
|
||||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
|
||||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||||
import projectSelect from '~/project_select';
|
import projectSelect from '~/project_select';
|
||||||
|
@ -8,7 +8,7 @@ import projectSelect from '~/project_select';
|
||||||
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
|
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
|
||||||
|
|
||||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_BULK_UPDATE_PREFIX);
|
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
|
||||||
|
|
||||||
initFilteredSearch({
|
initFilteredSearch({
|
||||||
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
page: FILTERED_SEARCH.MERGE_REQUESTS,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
|
||||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||||
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
||||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
|
||||||
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
|
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
|
||||||
import initManualOrdering from '~/issues/manual_ordering';
|
import initManualOrdering from '~/issues/manual_ordering';
|
||||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||||
|
@ -20,7 +20,8 @@ if (gon.features?.vueIssuesList) {
|
||||||
useDefaultState: true,
|
useDefaultState: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.ISSUE);
|
initBulkUpdateSidebar(ISSUABLE_INDEX.ISSUE);
|
||||||
|
initIssueStatusSelect();
|
||||||
new UsersSelect(); // eslint-disable-line no-new
|
new UsersSelect(); // eslint-disable-line no-new
|
||||||
|
|
||||||
initCsvImportExportButtons();
|
initCsvImportExportButtons();
|
||||||
|
|
|
@ -2,13 +2,14 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra
|
||||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||||
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||||
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
|
||||||
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
|
import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
|
||||||
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
import { FILTERED_SEARCH } from '~/filtered_search/constants';
|
||||||
import { ISSUABLE_INDEX } from '~/issuable/constants';
|
import { ISSUABLE_INDEX } from '~/issuable/constants';
|
||||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||||
import UsersSelect from '~/users_select';
|
import UsersSelect from '~/users_select';
|
||||||
|
|
||||||
issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.MERGE_REQUEST);
|
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
|
||||||
|
initIssueStatusSelect();
|
||||||
|
|
||||||
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
|
||||||
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
|
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
|
||||||
|
|
|
@ -54,7 +54,7 @@ class GroupsFinder < UnionFinder
|
||||||
groups = []
|
groups = []
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
if Feature.enabled?(:use_traversal_ids_groups_finder, default_enabled: :yaml)
|
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
|
||||||
groups << current_user.authorized_groups.self_and_ancestors
|
groups << current_user.authorized_groups.self_and_ancestors
|
||||||
groups << current_user.groups.self_and_descendants
|
groups << current_user.groups.self_and_descendants
|
||||||
else
|
else
|
||||||
|
@ -81,7 +81,7 @@ class GroupsFinder < UnionFinder
|
||||||
.groups
|
.groups
|
||||||
.where('members.access_level >= ?', params[:min_access_level])
|
.where('members.access_level >= ?', params[:min_access_level])
|
||||||
|
|
||||||
if Feature.enabled?(:use_traversal_ids_groups_finder, default_enabled: :yaml)
|
if Feature.enabled?(:use_traversal_ids_groups_finder, current_user, default_enabled: :yaml)
|
||||||
groups.self_and_descendants
|
groups.self_and_descendants
|
||||||
else
|
else
|
||||||
Gitlab::ObjectHierarchy
|
Gitlab::ObjectHierarchy
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
module BackgroundMigrationsHelper
|
module BackgroundMigrationsHelper
|
||||||
def batched_migration_status_badge_class_name(migration)
|
def batched_migration_status_badge_variant(migration)
|
||||||
class_names = {
|
variants = {
|
||||||
'active' => 'badge-info',
|
'active' => :info,
|
||||||
'paused' => 'badge-warning',
|
'paused' => :warning,
|
||||||
'failed' => 'badge-danger',
|
'failed' => :danger,
|
||||||
'finished' => 'badge-success'
|
'finished' => :success
|
||||||
}
|
}
|
||||||
|
|
||||||
class_names[migration.status]
|
variants[migration.status]
|
||||||
end
|
end
|
||||||
|
|
||||||
# The extra logic here is needed because total_tuple_count is just
|
# The extra logic here is needed because total_tuple_count is just
|
||||||
|
|
|
@ -42,16 +42,19 @@ module Ci
|
||||||
check_access!(build)
|
check_access!(build)
|
||||||
|
|
||||||
new_build = clone_build(build)
|
new_build = clone_build(build)
|
||||||
|
|
||||||
|
if create_deployment_in_separate_transaction?
|
||||||
|
new_build.run_after_commit do |new_build|
|
||||||
|
::Deployments::CreateForBuildService.new.execute(new_build)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
::Ci::Pipelines::AddJobService.new(build.pipeline).execute!(new_build) do |job|
|
::Ci::Pipelines::AddJobService.new(build.pipeline).execute!(new_build) do |job|
|
||||||
BulkInsertableAssociations.with_bulk_insert do
|
BulkInsertableAssociations.with_bulk_insert do
|
||||||
job.save!
|
job.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if create_deployment_in_separate_transaction?
|
|
||||||
clone_deployment!(new_build, build)
|
|
||||||
end
|
|
||||||
|
|
||||||
build.reset # refresh the data to get new values of `retried` and `processed`.
|
build.reset # refresh the data to get new values of `retried` and `processed`.
|
||||||
|
|
||||||
new_build
|
new_build
|
||||||
|
@ -95,20 +98,6 @@ module Ci
|
||||||
.deployment_attributes_for(new_build, old_build.persisted_environment)
|
.deployment_attributes_for(new_build, old_build.persisted_environment)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_deployment!(new_build, old_build)
|
|
||||||
return unless old_build.deployment.present?
|
|
||||||
|
|
||||||
# We should clone the previous deployment attributes instead of initializing
|
|
||||||
# new object with `Seed::Deployment`.
|
|
||||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/347206
|
|
||||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
|
||||||
.new(new_build, old_build.persisted_environment).to_resource
|
|
||||||
|
|
||||||
return unless deployment
|
|
||||||
|
|
||||||
new_build.create_deployment!(deployment.attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_deployment_in_separate_transaction?
|
def create_deployment_in_separate_transaction?
|
||||||
strong_memoize(:create_deployment_in_separate_transaction) do
|
strong_memoize(:create_deployment_in_separate_transaction) do
|
||||||
::Feature.enabled?(:create_deployment_in_separate_transaction, project, default_enabled: :yaml)
|
::Feature.enabled?(:create_deployment_in_separate_transaction, project, default_enabled: :yaml)
|
||||||
|
|
25
app/services/deployments/create_for_build_service.rb
Normal file
25
app/services/deployments/create_for_build_service.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Deployments
|
||||||
|
# This class creates a deployment record for a build (a pipeline job).
|
||||||
|
class CreateForBuildService
|
||||||
|
DeploymentCreationError = Class.new(StandardError)
|
||||||
|
|
||||||
|
def execute(build)
|
||||||
|
return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
|
||||||
|
|
||||||
|
# TODO: Move all buisness logic in `Seed::Deployment` to this class after
|
||||||
|
# `create_deployment_in_separate_transaction` feature flag has been removed.
|
||||||
|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/348778
|
||||||
|
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
||||||
|
.new(build, build.persisted_environment).to_resource
|
||||||
|
|
||||||
|
return unless deployment
|
||||||
|
|
||||||
|
build.create_deployment!(deployment.attributes)
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
||||||
|
DeploymentCreationError.new(e.message), build_id: build.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,7 @@
|
||||||
- else
|
- else
|
||||||
= _('Unknown')
|
= _('Unknown')
|
||||||
%td{ role: 'cell', data: { label: _('Status') } }
|
%td{ role: 'cell', data: { label: _('Status') } }
|
||||||
%span.badge.badge-pill.gl-badge.sm{ class: batched_migration_status_badge_class_name(migration) }= migration.status.humanize
|
= gl_badge_tag migration.status.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
|
||||||
%td{ role: 'cell', data: { label: _('Action') } }
|
%td{ role: 'cell', data: { label: _('Action') } }
|
||||||
- if migration.active?
|
- if migration.active?
|
||||||
= button_to pause_admin_background_migration_path(migration),
|
= button_to pause_admin_background_migration_path(migration),
|
||||||
|
|
|
@ -8,18 +8,15 @@
|
||||||
%li.nav-item{ role: 'presentation' }
|
%li.nav-item{ role: 'presentation' }
|
||||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path, class: (active_tab_classes if @current_tab == 'queued'), role: 'tab' }
|
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path, class: (active_tab_classes if @current_tab == 'queued'), role: 'tab' }
|
||||||
= _('Queued')
|
= _('Queued')
|
||||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['queued'])
|
||||||
= limited_counter_with_delimiter(@relations_by_tab['queued'])
|
|
||||||
%li.nav-item{ role: 'presentation' }
|
%li.nav-item{ role: 'presentation' }
|
||||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'failed'), class: (active_tab_classes if @current_tab == 'failed'), role: 'tab' }
|
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'failed'), class: (active_tab_classes if @current_tab == 'failed'), role: 'tab' }
|
||||||
= _('Failed')
|
= _('Failed')
|
||||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['failed'])
|
||||||
= limited_counter_with_delimiter(@relations_by_tab['failed'])
|
|
||||||
%li.nav-item{ role: 'presentation' }
|
%li.nav-item{ role: 'presentation' }
|
||||||
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'finished'), class: (active_tab_classes if @current_tab == 'finished'), role: 'tab' }
|
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'finished'), class: (active_tab_classes if @current_tab == 'finished'), role: 'tab' }
|
||||||
= _('Finished')
|
= _('Finished')
|
||||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
|
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['finished'])
|
||||||
= limited_counter_with_delimiter(@relations_by_tab['finished'])
|
|
||||||
|
|
||||||
.tab-content.gl-tab-content
|
.tab-content.gl-tab-content
|
||||||
.tab-pane.active{ role: 'tabpanel' }
|
.tab-pane.active{ role: 'tabpanel' }
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
|
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
|
||||||
= gl_tab_link_to todos_filter_path(state: 'pending'), item_active: params[:state].blank? || params[:state] == 'pending', class: "js-todos-pending" do
|
= gl_tab_link_to todos_filter_path(state: 'pending'), item_active: params[:state].blank? || params[:state] == 'pending', class: "js-todos-pending" do
|
||||||
= _("To Do")
|
= _("To Do")
|
||||||
= gl_tab_counter_badge number_with_delimiter(todos_pending_count)
|
= gl_tab_counter_badge(number_with_delimiter(todos_pending_count), { class: 'js-todos-badge' })
|
||||||
= gl_tab_link_to todos_filter_path(state: 'done'), item_active: params[:state] == 'done', class: "js-todos-done" do
|
= gl_tab_link_to todos_filter_path(state: 'done'), item_active: params[:state] == 'done', class: "js-todos-done" do
|
||||||
= _("Done")
|
= _("Done")
|
||||||
= gl_tab_counter_badge number_with_delimiter(todos_done_count)
|
= gl_tab_counter_badge(number_with_delimiter(todos_done_count), { class: 'js-todos-badge' })
|
||||||
|
|
||||||
.nav-controls
|
.nav-controls
|
||||||
- if @allowed_todos.any?(&:pending?)
|
- if @allowed_todos.any?(&:pending?)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: redis_hll_counters.testing.users_clicking_license_testing_visiting_external_website_monthly
|
||||||
|
description: Count of users clicking licence to visit external information website
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: group::static analysis
|
||||||
|
product_category: dependency_scanning
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '14.7'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76917
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- users_clicking_license_testing_visiting_external_website
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: redis_hll_counters.testing.users_clicking_license_testing_visiting_external_website_weekly
|
||||||
|
description: Count of users clicking licence to visit external information website
|
||||||
|
product_section: sec
|
||||||
|
product_stage: secure
|
||||||
|
product_group: group::static analysis
|
||||||
|
product_category: dependency_scanning
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '14.7'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76917
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- users_clicking_license_testing_visiting_external_website
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -23,7 +23,7 @@ Side effects:
|
||||||
|
|
||||||
## `ci_queueing_disaster_recovery_disable_quota`
|
## `ci_queueing_disaster_recovery_disable_quota`
|
||||||
|
|
||||||
This feature flag, if temporarily enabled, disables enforcing CI minutes quota
|
This feature flag, if temporarily enabled, disables enforcing CI/CD minutes quota
|
||||||
on shared runners. This can help to reduce system resource usage on the
|
on shared runners. This can help to reduce system resource usage on the
|
||||||
`jobs/request` endpoint by significantly reducing the computations being
|
`jobs/request` endpoint by significantly reducing the computations being
|
||||||
performed.
|
performed.
|
||||||
|
|
|
@ -34,6 +34,7 @@ The following API resources are available in the project context:
|
||||||
| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) |
|
| [Debian distributions](packages/debian_project_distributions.md) | `/projects/:id/debian_distributions` (also available for groups) |
|
||||||
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
|
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
|
||||||
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
|
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
|
||||||
|
| [Deploy tokens](deploy_tokens.md) | `/projects/:id/deploy_tokens` (also available for groups and standalone) |
|
||||||
| [Deployments](deployments.md) | `/projects/:id/deployments` |
|
| [Deployments](deployments.md) | `/projects/:id/deployments` |
|
||||||
| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) |
|
| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) |
|
||||||
| [Environments](environments.md) | `/projects/:id/environments` |
|
| [Environments](environments.md) | `/projects/:id/environments` |
|
||||||
|
@ -101,6 +102,7 @@ The following API resources are available in the group context:
|
||||||
| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
|
| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) |
|
||||||
| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
|
| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) |
|
||||||
| [Debian distributions](packages/debian_group_distributions.md) | `/groups/:id/-/packages/debian` (also available for projects) |
|
| [Debian distributions](packages/debian_group_distributions.md) | `/groups/:id/-/packages/debian` (also available for projects) |
|
||||||
|
| [Deploy tokens](deploy_tokens.md) | `/groups/:id/deploy_tokens` (also available for projects and standalone) |
|
||||||
| [Discussions](discussions.md) (threaded comments) **(ULTIMATE)** | `/groups/:id/epics/.../discussions` (also available for projects) |
|
| [Discussions](discussions.md) (threaded comments) **(ULTIMATE)** | `/groups/:id/epics/.../discussions` (also available for projects) |
|
||||||
| [Epic issues](epic_issues.md) **(ULTIMATE)** | `/groups/:id/epics/.../issues` |
|
| [Epic issues](epic_issues.md) **(ULTIMATE)** | `/groups/:id/epics/.../issues` |
|
||||||
| [Epic links](epic_links.md) **(ULTIMATE)** | `/groups/:id/epics/.../epics` |
|
| [Epic links](epic_links.md) **(ULTIMATE)** | `/groups/:id/epics/.../epics` |
|
||||||
|
@ -137,6 +139,7 @@ The following API resources are available outside of project and group contexts
|
||||||
| [Code snippets](snippets.md) | `/snippets` |
|
| [Code snippets](snippets.md) | `/snippets` |
|
||||||
| [Custom attributes](custom_attributes.md) | `/users/:id/custom_attributes` (also available for groups and projects) |
|
| [Custom attributes](custom_attributes.md) | `/users/:id/custom_attributes` (also available for groups and projects) |
|
||||||
| [Deploy keys](deploy_keys.md) | `/deploy_keys` (also available for projects) |
|
| [Deploy keys](deploy_keys.md) | `/deploy_keys` (also available for projects) |
|
||||||
|
| [Deploy tokens](deploy_tokens.md) | `/deploy_tokens` (also available for projects and groups) |
|
||||||
| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) |
|
| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) |
|
||||||
| [Feature flags](features.md) | `/features` |
|
| [Feature flags](features.md) | `/features` |
|
||||||
| [Geo Nodes](geo_nodes.md) **(PREMIUM SELF)** | `/geo_nodes` |
|
| [Geo Nodes](geo_nodes.md) **(PREMIUM SELF)** | `/geo_nodes` |
|
||||||
|
|
|
@ -385,7 +385,7 @@ listed in the descriptions of the relevant settings.
|
||||||
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
|
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
|
||||||
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
|
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
|
||||||
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
|
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
|
||||||
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
|
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of CI/CD minutes that a group can use on shared runners per month. |
|
||||||
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
|
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
|
||||||
| `sidekiq_job_limiter_mode` | string | no | `track` or `compress`. Sets the behavior for [Sidekiq job size limits](../user/admin_area/settings/sidekiq_job_limits.md). Default: 'compress'. |
|
| `sidekiq_job_limiter_mode` | string | no | `track` or `compress`. Sets the behavior for [Sidekiq job size limits](../user/admin_area/settings/sidekiq_job_limits.md). Default: 'compress'. |
|
||||||
| `sidekiq_job_limiter_compression_threshold_bytes` | integer | no | The threshold in bytes at which Sidekiq jobs are compressed before being stored in Redis. Default: 100 000 bytes (100KB). |
|
| `sidekiq_job_limiter_compression_threshold_bytes` | integer | no | The threshold in bytes at which Sidekiq jobs are compressed before being stored in Redis. Default: 100 000 bytes (100KB). |
|
||||||
|
|
|
@ -257,14 +257,33 @@ WARNING:
|
||||||
Deleting a pipeline expires all pipeline caches, and deletes all related objects,
|
Deleting a pipeline expires all pipeline caches, and deletes all related objects,
|
||||||
such as builds, logs, artifacts, and triggers. **This action cannot be undone.**
|
such as builds, logs, artifacts, and triggers. **This action cannot be undone.**
|
||||||
|
|
||||||
### Quotas of CI/CD minutes
|
### Pipeline security on protected branches
|
||||||
|
|
||||||
Each user has a personal pipeline quota that tracks the usage of shared runners in all personal projects.
|
A strict security model is enforced when pipelines are executed on
|
||||||
Each group has a [quota of CI/CD minutes](cicd_minutes.md) that tracks the usage of shared runners for all projects created within the group.
|
[protected branches](../../user/project/protected_branches.md).
|
||||||
|
|
||||||
When a pipeline is triggered, regardless of who triggered it, the quota of CI/CD minutes for the project owner's [namespace](../../user/group/index.md#namespaces) is used. In this case, the namespace can be the user or group that owns the project.
|
The following actions are allowed on protected branches only if the user is
|
||||||
|
[allowed to merge or push](../../user/project/protected_branches.md)
|
||||||
|
on that specific branch:
|
||||||
|
|
||||||
#### How pipeline duration is calculated
|
- Run manual pipelines (using the [Web UI](#run-a-pipeline-manually) or [pipelines API](#pipelines-api)).
|
||||||
|
- Run scheduled pipelines.
|
||||||
|
- Run pipelines using triggers.
|
||||||
|
- Run on-demand DAST scan.
|
||||||
|
- Trigger manual actions on existing pipelines.
|
||||||
|
- Retry or cancel existing jobs (using the Web UI or pipelines API).
|
||||||
|
|
||||||
|
**Variables** marked as **protected** are accessible only to jobs that
|
||||||
|
run on protected branches, preventing untrusted users getting unintended access to
|
||||||
|
sensitive information like deployment credentials and tokens.
|
||||||
|
|
||||||
|
**Runners** marked as **protected** can run jobs only on protected
|
||||||
|
branches, preventing untrusted code from executing on the protected runner and
|
||||||
|
preserving deployment keys and other credentials from being unintentionally
|
||||||
|
accessed. In order to ensure that jobs intended to be executed on protected
|
||||||
|
runners do not use regular runners, they must be tagged accordingly.
|
||||||
|
|
||||||
|
### How pipeline duration is calculated
|
||||||
|
|
||||||
Total running time for a given pipeline excludes retries and pending
|
Total running time for a given pipeline excludes retries and pending
|
||||||
(queued) time.
|
(queued) time.
|
||||||
|
@ -301,44 +320,6 @@ The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time
|
||||||
(4 - 1) + (7 - 6) => 4
|
(4 - 1) + (7 - 6) => 4
|
||||||
```
|
```
|
||||||
|
|
||||||
#### How pipeline quota usage is calculated
|
|
||||||
|
|
||||||
Pipeline quota usage is calculated as the sum of the duration of each individual job. This is slightly different to how pipeline _duration_ is [calculated](#how-pipeline-duration-is-calculated). Pipeline quota usage doesn't consider any overlap of jobs running in parallel.
|
|
||||||
|
|
||||||
For example, a pipeline consists of the following jobs:
|
|
||||||
|
|
||||||
- Job A takes 3 minutes.
|
|
||||||
- Job B takes 3 minutes.
|
|
||||||
- Job C takes 2 minutes.
|
|
||||||
|
|
||||||
The pipeline quota usage is the sum of each job's duration. In this example, 8 runner minutes would be used, calculated as: 3 + 3 + 2.
|
|
||||||
|
|
||||||
### Pipeline security on protected branches
|
|
||||||
|
|
||||||
A strict security model is enforced when pipelines are executed on
|
|
||||||
[protected branches](../../user/project/protected_branches.md).
|
|
||||||
|
|
||||||
The following actions are allowed on protected branches only if the user is
|
|
||||||
[allowed to merge or push](../../user/project/protected_branches.md)
|
|
||||||
on that specific branch:
|
|
||||||
|
|
||||||
- Run manual pipelines (using the [Web UI](#run-a-pipeline-manually) or [pipelines API](#pipelines-api)).
|
|
||||||
- Run scheduled pipelines.
|
|
||||||
- Run pipelines using triggers.
|
|
||||||
- Run on-demand DAST scan.
|
|
||||||
- Trigger manual actions on existing pipelines.
|
|
||||||
- Retry or cancel existing jobs (using the Web UI or pipelines API).
|
|
||||||
|
|
||||||
**Variables** marked as **protected** are accessible only to jobs that
|
|
||||||
run on protected branches, preventing untrusted users getting unintended access to
|
|
||||||
sensitive information like deployment credentials and tokens.
|
|
||||||
|
|
||||||
**Runners** marked as **protected** can run jobs only on protected
|
|
||||||
branches, preventing untrusted code from executing on the protected runner and
|
|
||||||
preserving deployment keys and other credentials from being unintentionally
|
|
||||||
accessed. In order to ensure that jobs intended to be executed on protected
|
|
||||||
runners do not use regular runners, they must be tagged accordingly.
|
|
||||||
|
|
||||||
## Visualize pipelines
|
## Visualize pipelines
|
||||||
|
|
||||||
Pipelines can be complex structures with many sequential and parallel jobs.
|
Pipelines can be complex structures with many sequential and parallel jobs.
|
||||||
|
|
|
@ -28,7 +28,7 @@ If you are using a self-managed instance of GitLab:
|
||||||
going to your project's **Settings > CI/CD**, expanding the **Runners** section,
|
going to your project's **Settings > CI/CD**, expanding the **Runners** section,
|
||||||
and clicking **Show runner installation instructions**.
|
and clicking **Show runner installation instructions**.
|
||||||
These instructions are also available [in the documentation](https://docs.gitlab.com/runner/install/index.html).
|
These instructions are also available [in the documentation](https://docs.gitlab.com/runner/install/index.html).
|
||||||
- The administrator can also configure a maximum number of shared runner [pipeline minutes for
|
- The administrator can also configure a maximum number of shared runner [CI/CD minutes for
|
||||||
each group](../pipelines/cicd_minutes.md#set-the-quota-of-cicd-minutes-for-a-specific-namespace).
|
each group](../pipelines/cicd_minutes.md#set-the-quota-of-cicd-minutes-for-a-specific-namespace).
|
||||||
|
|
||||||
If you are using GitLab.com:
|
If you are using GitLab.com:
|
||||||
|
|
|
@ -180,7 +180,7 @@ feature and its components work.
|
||||||
Watch a walkthrough of this feature in details in the video below.
|
Watch a walkthrough of this feature in details in the video below.
|
||||||
|
|
||||||
<div class="video-fallback">
|
<div class="video-fallback">
|
||||||
See the video: <a href="https://www.youtube.com/watch?v=NmdWRGT8kZg">CI Minutes - architectural overview</a>.
|
See the video: <a href="https://www.youtube.com/watch?v=NmdWRGT8kZg">CI/CD minutes - architectural overview</a>.
|
||||||
</div>
|
</div>
|
||||||
<figure class="video-container">
|
<figure class="video-container">
|
||||||
<iframe src="https://www.youtube.com/embed/NmdWRGT8kZg" frameborder="0" allowfullscreen="true"> </iframe>
|
<iframe src="https://www.youtube.com/embed/NmdWRGT8kZg" frameborder="0" allowfullscreen="true"> </iframe>
|
||||||
|
|
|
@ -182,6 +182,11 @@ Use **check out** as a verb. For the Git command, use `checkout`.
|
||||||
|
|
||||||
CI/CD is always uppercase. No need to spell it out on first use.
|
CI/CD is always uppercase. No need to spell it out on first use.
|
||||||
|
|
||||||
|
## CI/CD minutes
|
||||||
|
|
||||||
|
Use **CI/CD minutes** instead of **CI minutes**, **pipeline minutes**, **pipeline minutes quota**, or
|
||||||
|
**CI pipeline minutes**. This decision was made in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342813).
|
||||||
|
|
||||||
## click
|
## click
|
||||||
|
|
||||||
Do not use **click**. Instead, use **select** with buttons, links, menu items, and lists.
|
Do not use **click**. Instead, use **select** with buttons, links, menu items, and lists.
|
||||||
|
|
|
@ -703,10 +703,10 @@ Example response:
|
||||||
|
|
||||||
- CustomersDot
|
- CustomersDot
|
||||||
|
|
||||||
## CI minute provisioning
|
## CI/CD minutes provisioning
|
||||||
|
|
||||||
The CI Minute endpoints are used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
|
The CI/CD Minutes endpoints are used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
|
||||||
to apply additional packs of CI minutes, for personal namespaces or top-level groups within GitLab.com.
|
to apply additional packs of CI/CD minutes, for personal namespaces or top-level groups within GitLab.com.
|
||||||
|
|
||||||
### Creating an additional pack
|
### Creating an additional pack
|
||||||
|
|
||||||
|
|
|
@ -177,7 +177,7 @@ To change the password for this customers portal account:
|
||||||
### GitLab for Education
|
### GitLab for Education
|
||||||
|
|
||||||
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
|
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
|
||||||
the top GitLab tier, plus 50,000 CI minutes per month.
|
the top GitLab tier, plus 50,000 CI/CD minutes per month.
|
||||||
|
|
||||||
The GitLab for Education license can only be used for instructional-use or
|
The GitLab for Education license can only be used for instructional-use or
|
||||||
non-commercial academic research.
|
non-commercial academic research.
|
||||||
|
@ -188,7 +188,7 @@ Find more information on how to apply and renew at
|
||||||
### GitLab for Open Source
|
### GitLab for Open Source
|
||||||
|
|
||||||
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
|
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
|
||||||
the top GitLab tier, plus 50,000 CI minutes per month.
|
the top GitLab tier, plus 50,000 CI/CD minutes per month.
|
||||||
|
|
||||||
You can find more information about the [program requirements](https://about.gitlab.com/solutions/open-source/join/#requirements),
|
You can find more information about the [program requirements](https://about.gitlab.com/solutions/open-source/join/#requirements),
|
||||||
[renewals](https://about.gitlab.com/solutions/open-source/join/#renewals),
|
[renewals](https://about.gitlab.com/solutions/open-source/join/#renewals),
|
||||||
|
@ -253,7 +253,7 @@ if a project holds sensitive data. Email `opensource@gitlab.com` with details of
|
||||||
### GitLab for Startups
|
### GitLab for Startups
|
||||||
|
|
||||||
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
|
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
|
||||||
the top GitLab tier, plus 50,000 CI minutes per month for 12 months.
|
the top GitLab tier, plus 50,000 CI/CD minutes per month for 12 months.
|
||||||
|
|
||||||
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
|
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,8 @@ The following data is included in the export:
|
||||||
- Access level ([Project](../permissions.md#project-members-permissions) and [Group](../permissions.md#group-members-permissions))
|
- Access level ([Project](../permissions.md#project-members-permissions) and [Group](../permissions.md#group-members-permissions))
|
||||||
- Date of last activity ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345388) in GitLab 14.6). For a list of activities that populate this column, see the [Users API documentation](../../api/users.md#get-user-activities-admin-only).
|
- Date of last activity ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345388) in GitLab 14.6). For a list of activities that populate this column, see the [Users API documentation](../../api/users.md#get-user-activities-admin-only).
|
||||||
|
|
||||||
|
Only the first 100,000 user accounts are exported.
|
||||||
|
|
||||||
![user permission export button](img/export_permissions_v13_11.png)
|
![user permission export button](img/export_permissions_v13_11.png)
|
||||||
|
|
||||||
#### Users statistics
|
#### Users statistics
|
||||||
|
|
|
@ -22,6 +22,10 @@ NOTE:
|
||||||
By default, all Git operations are first tried unauthenticated. Because of this, HTTP Git operations
|
By default, all Git operations are first tried unauthenticated. Because of this, HTTP Git operations
|
||||||
may trigger the rate limits configured for unauthenticated requests.
|
may trigger the rate limits configured for unauthenticated requests.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
The rate limits for API requests don't affect requests made by the frontend, as these are always
|
||||||
|
counted as web traffic.
|
||||||
|
|
||||||
## Enable unauthenticated API request rate limit
|
## Enable unauthenticated API request rate limit
|
||||||
|
|
||||||
To enable the unauthenticated request rate limit:
|
To enable the unauthenticated request rate limit:
|
||||||
|
|
|
@ -774,7 +774,7 @@ To view the merge request approval rules for a group:
|
||||||
- [Webhooks](../project/integrations/webhooks.md).
|
- [Webhooks](../project/integrations/webhooks.md).
|
||||||
- [Kubernetes cluster integration](clusters/index.md).
|
- [Kubernetes cluster integration](clusters/index.md).
|
||||||
- [Audit Events](../../administration/audit_events.md#group-events).
|
- [Audit Events](../../administration/audit_events.md#group-events).
|
||||||
- [Pipelines quota](../admin_area/settings/continuous_integration.md): Keep track of the pipeline quota for the group.
|
- [CI/CD minutes quota](../../ci/pipelines/cicd_minutes.md): Keep track of the CI/CD minute quota for the group.
|
||||||
- [Integrations](../admin_area/settings/project_integration_management.md).
|
- [Integrations](../admin_area/settings/project_integration_management.md).
|
||||||
- [Transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace).
|
- [Transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace).
|
||||||
- [Share a project with a group](../project/members/share_project_with_groups.md): Give all group members access to the project at once.
|
- [Share a project with a group](../project/members/share_project_with_groups.md): Give all group members access to the project at once.
|
||||||
|
|
|
@ -207,7 +207,7 @@ This improvement is [tracked as a follow-up](https://gitlab.com/gitlab-org/gitla
|
||||||
|
|
||||||
- When working locally in your branch, add multiple commits and only push when
|
- When working locally in your branch, add multiple commits and only push when
|
||||||
you're done, so GitLab runs only one pipeline for all the commits pushed
|
you're done, so GitLab runs only one pipeline for all the commits pushed
|
||||||
at once. By doing so, you save pipeline minutes.
|
at once. By doing so, you save CI/CD minutes.
|
||||||
- Delete feature branches on merge or after merging them to keep your repository clean.
|
- Delete feature branches on merge or after merging them to keep your repository clean.
|
||||||
- Take one thing at a time and ship the smallest changes possible. By doing so,
|
- Take one thing at a time and ship the smallest changes possible. By doing so,
|
||||||
reviews are faster and your changes are less prone to errors.
|
reviews are faster and your changes are less prone to errors.
|
||||||
|
|
|
@ -5,8 +5,6 @@ module Gitlab
|
||||||
module Pipeline
|
module Pipeline
|
||||||
module Chain
|
module Chain
|
||||||
class CreateDeployments < Chain::Base
|
class CreateDeployments < Chain::Base
|
||||||
DeploymentCreationError = Class.new(StandardError)
|
|
||||||
|
|
||||||
def perform!
|
def perform!
|
||||||
return unless pipeline.create_deployment_in_separate_transaction?
|
return unless pipeline.create_deployment_in_separate_transaction?
|
||||||
|
|
||||||
|
@ -24,18 +22,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_deployment(build)
|
def create_deployment(build)
|
||||||
return unless build.instance_of?(::Ci::Build) && build.persisted_environment.present?
|
::Deployments::CreateForBuildService.new.execute(build)
|
||||||
|
|
||||||
deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
|
|
||||||
.new(build, build.persisted_environment).to_resource
|
|
||||||
|
|
||||||
return unless deployment
|
|
||||||
|
|
||||||
deployment.deployable = build
|
|
||||||
deployment.save!
|
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
|
||||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
|
||||||
DeploymentCreationError.new(e.message), build_id: build.id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module RackAttack
|
module RackAttack
|
||||||
module Request
|
module Request
|
||||||
|
include ::Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
FILES_PATH_REGEX = %r{^/api/v\d+/projects/[^/]+/repository/files/.+}.freeze
|
FILES_PATH_REGEX = %r{^/api/v\d+/projects/[^/]+/repository/files/.+}.freeze
|
||||||
GROUP_PATH_REGEX = %r{^/api/v\d+/groups/[^/]+/?$}.freeze
|
GROUP_PATH_REGEX = %r{^/api/v\d+/groups/[^/]+/?$}.freeze
|
||||||
|
|
||||||
|
@ -30,15 +32,15 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_internal_request?
|
def api_internal_request?
|
||||||
path =~ %r{^/api/v\d+/internal/}
|
path.match?(%r{^/api/v\d+/internal/})
|
||||||
end
|
end
|
||||||
|
|
||||||
def health_check_request?
|
def health_check_request?
|
||||||
path =~ %r{^/-/(health|liveness|readiness|metrics)}
|
path.match?(%r{^/-/(health|liveness|readiness|metrics)})
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_registry_event?
|
def container_registry_event?
|
||||||
path =~ %r{^/api/v\d+/container_registry_event/}
|
path.match?(%r{^/api/v\d+/container_registry_event/})
|
||||||
end
|
end
|
||||||
|
|
||||||
def product_analytics_collector_request?
|
def product_analytics_collector_request?
|
||||||
|
@ -58,7 +60,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def protected_path_regex
|
def protected_path_regex
|
||||||
path =~ protected_paths_regex
|
path.match?(protected_paths_regex)
|
||||||
end
|
end
|
||||||
|
|
||||||
def throttle?(throttle, authenticated:)
|
def throttle?(throttle, authenticated:)
|
||||||
|
@ -70,6 +72,7 @@ module Gitlab
|
||||||
def throttle_unauthenticated_api?
|
def throttle_unauthenticated_api?
|
||||||
api_request? &&
|
api_request? &&
|
||||||
!should_be_skipped? &&
|
!should_be_skipped? &&
|
||||||
|
!frontend_request? &&
|
||||||
!throttle_unauthenticated_packages_api? &&
|
!throttle_unauthenticated_packages_api? &&
|
||||||
!throttle_unauthenticated_files_api? &&
|
!throttle_unauthenticated_files_api? &&
|
||||||
!throttle_unauthenticated_deprecated_api? &&
|
!throttle_unauthenticated_deprecated_api? &&
|
||||||
|
@ -78,7 +81,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def throttle_unauthenticated_web?
|
def throttle_unauthenticated_web?
|
||||||
web_request? &&
|
(web_request? || frontend_request?) &&
|
||||||
!should_be_skipped? &&
|
!should_be_skipped? &&
|
||||||
# TODO: Column will be renamed in https://gitlab.com/gitlab-org/gitlab/-/issues/340031
|
# TODO: Column will be renamed in https://gitlab.com/gitlab-org/gitlab/-/issues/340031
|
||||||
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
|
||||||
|
@ -87,6 +90,7 @@ module Gitlab
|
||||||
|
|
||||||
def throttle_authenticated_api?
|
def throttle_authenticated_api?
|
||||||
api_request? &&
|
api_request? &&
|
||||||
|
!frontend_request? &&
|
||||||
!throttle_authenticated_packages_api? &&
|
!throttle_authenticated_packages_api? &&
|
||||||
!throttle_authenticated_files_api? &&
|
!throttle_authenticated_files_api? &&
|
||||||
!throttle_authenticated_deprecated_api? &&
|
!throttle_authenticated_deprecated_api? &&
|
||||||
|
@ -94,7 +98,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def throttle_authenticated_web?
|
def throttle_authenticated_web?
|
||||||
web_request? &&
|
(web_request? || frontend_request?) &&
|
||||||
!throttle_authenticated_git_lfs? &&
|
!throttle_authenticated_git_lfs? &&
|
||||||
Gitlab::Throttle.settings.throttle_authenticated_web_enabled
|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled
|
||||||
end
|
end
|
||||||
|
@ -178,15 +182,24 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def packages_api_path?
|
def packages_api_path?
|
||||||
path =~ ::Gitlab::Regex::Packages::API_PATH_REGEX
|
path.match?(::Gitlab::Regex::Packages::API_PATH_REGEX)
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_lfs_path?
|
def git_lfs_path?
|
||||||
path =~ Gitlab::PathRegex.repository_git_lfs_route_regex
|
path.match?(Gitlab::PathRegex.repository_git_lfs_route_regex)
|
||||||
end
|
end
|
||||||
|
|
||||||
def files_api_path?
|
def files_api_path?
|
||||||
path =~ FILES_PATH_REGEX
|
path.match?(FILES_PATH_REGEX)
|
||||||
|
end
|
||||||
|
|
||||||
|
def frontend_request?
|
||||||
|
strong_memoize(:frontend_request) do
|
||||||
|
next false unless env.include?('HTTP_X_CSRF_TOKEN') && session.include?(:_csrf_token)
|
||||||
|
|
||||||
|
# CSRF tokens are not verified for GET/HEAD requests, so we pretend that we always have a POST request.
|
||||||
|
Gitlab::RequestForgeryProtection.verified?(env.merge('REQUEST_METHOD' => 'POST'))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def deprecated_api_request?
|
def deprecated_api_request?
|
||||||
|
@ -195,7 +208,7 @@ module Gitlab
|
||||||
with_projects = params['with_projects']
|
with_projects = params['with_projects']
|
||||||
with_projects = true if with_projects.blank?
|
with_projects = true if with_projects.blank?
|
||||||
|
|
||||||
path =~ GROUP_PATH_REGEX && Gitlab::Utils.to_boolean(with_projects)
|
path.match?(GROUP_PATH_REGEX) && Gitlab::Utils.to_boolean(with_projects)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -368,6 +368,10 @@
|
||||||
redis_slot: testing
|
redis_slot: testing
|
||||||
category: testing
|
category: testing
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
|
- name: users_clicking_license_testing_visiting_external_website
|
||||||
|
redis_slot: testing
|
||||||
|
category: testing
|
||||||
|
aggregation: weekly
|
||||||
# Container Security - Network Policies
|
# Container Security - Network Policies
|
||||||
- name: clusters_using_network_policies_ui
|
- name: clusters_using_network_policies_ui
|
||||||
redis_slot: network_policies
|
redis_slot: network_policies
|
||||||
|
|
|
@ -2768,7 +2768,7 @@ msgstr ""
|
||||||
msgid "AdminUsers|Delete user and contributions"
|
msgid "AdminUsers|Delete user and contributions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AdminUsers|Export permissions as CSV"
|
msgid "AdminUsers|Export permissions as CSV (max 100,000 users)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AdminUsers|External"
|
msgid "AdminUsers|External"
|
||||||
|
|
|
@ -179,6 +179,30 @@ module QA
|
||||||
QA::Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval, &block)
|
QA::Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Object comparison
|
||||||
|
#
|
||||||
|
# @param [QA::Resource::Base] other
|
||||||
|
# @return [Boolean]
|
||||||
|
def ==(other)
|
||||||
|
other.is_a?(self.class) && comparable == other.comparable
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override inspect for a better rspec failure diff output
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def inspect
|
||||||
|
JSON.pretty_generate(comparable)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Custom resource comparison logic using resource attributes from api_resource
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def comparable
|
||||||
|
raise("comparable method needs to be implemented in order to compare resources via '=='")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def attribute_value(name, block)
|
def attribute_value(name, block)
|
||||||
|
|
|
@ -39,27 +39,12 @@ module QA
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def resource_web_url(_resource); end
|
def resource_web_url(_resource); end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::GroupBadge] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(GroupBadge) && comparable_badge == other.comparable_badge
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_badge)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing badges
|
# Return subset of fields for comparing badges
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_badge
|
def comparable
|
||||||
reload! unless api_response
|
reload! unless api_response
|
||||||
|
|
||||||
api_response.slice(
|
api_response.slice(
|
||||||
|
|
|
@ -123,18 +123,12 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
# Object comparison
|
||||||
|
# Override to make sure we are comparing descendands of GroupBase
|
||||||
#
|
#
|
||||||
# @param [QA::Resource::GroupBase] other
|
# @param [QA::Resource::GroupBase] other
|
||||||
# @return [Boolean]
|
# @return [Boolean]
|
||||||
def ==(other)
|
def ==(other)
|
||||||
other.is_a?(GroupBase) && comparable_group == other.comparable_group
|
other.is_a?(GroupBase) && comparable == other.comparable
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_group)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
@ -142,7 +136,7 @@ module QA
|
||||||
# Return subset of fields for comparing groups
|
# Return subset of fields for comparing groups
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_group
|
def comparable
|
||||||
reload! if api_response.nil?
|
reload! if api_response.nil?
|
||||||
|
|
||||||
api_resource.slice(
|
api_resource.slice(
|
||||||
|
|
|
@ -56,27 +56,12 @@ module QA
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::GroupMilestone] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(GroupMilestone) && comparable_milestone == other.comparable_milestone
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_milestone)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing milestones
|
# Return subset of fields for comparing milestones
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_milestone
|
def comparable
|
||||||
reload! unless api_response
|
reload! unless api_response
|
||||||
|
|
||||||
api_response.slice(
|
api_response.slice(
|
||||||
|
|
|
@ -95,27 +95,12 @@ module QA
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::Issue] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(Issue) && comparable_issue == other.comparable_issue
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_issue)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing issues
|
# Return subset of fields for comparing issues
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_issue
|
def comparable
|
||||||
reload! if api_response.nil?
|
reload! if api_response.nil?
|
||||||
|
|
||||||
api_resource.slice(
|
api_resource.slice(
|
||||||
|
|
|
@ -49,27 +49,12 @@ module QA
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::GroupBase] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(LabelBase) && comparable_label == other.comparable_label
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_label)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing labels
|
# Return subset of fields for comparing labels
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_label
|
def comparable
|
||||||
reload! unless api_response
|
reload! unless api_response
|
||||||
|
|
||||||
api_response.slice(
|
api_response.slice(
|
||||||
|
|
|
@ -168,27 +168,12 @@ module QA
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::MergeRequest] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(MergeRequest) && comparable_mr == other.comparable_mr
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_mr)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing merge requests
|
# Return subset of fields for comparing merge requests
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_mr
|
def comparable
|
||||||
reload! if api_response.nil?
|
reload! if api_response.nil?
|
||||||
|
|
||||||
api_resource.except(
|
api_resource.except(
|
||||||
|
|
|
@ -372,27 +372,12 @@ module QA
|
||||||
api_post_to(api_wikis_path, title: title, content: content)
|
api_post_to(api_wikis_path, title: title, content: content)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Object comparison
|
|
||||||
#
|
|
||||||
# @param [QA::Resource::Project] other
|
|
||||||
# @return [Boolean]
|
|
||||||
def ==(other)
|
|
||||||
other.is_a?(Project) && comparable_project == other.comparable_project
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override inspect for a better rspec failure diff output
|
|
||||||
#
|
|
||||||
# @return [String]
|
|
||||||
def inspect
|
|
||||||
JSON.pretty_generate(comparable_project)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Return subset of fields for comparing projects
|
# Return subset of fields for comparing projects
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def comparable_project
|
def comparable
|
||||||
reload! if api_response.nil?
|
reload! if api_response.nil?
|
||||||
|
|
||||||
api_resource.slice(
|
api_resource.slice(
|
||||||
|
|
|
@ -181,6 +181,15 @@ module QA
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Compare users by username and password
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def comparable
|
||||||
|
[username, password]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ldap_post_body
|
def ldap_post_body
|
||||||
|
|
|
@ -84,7 +84,8 @@ module QA
|
||||||
retry_attempts: example.metadata[:retry_attempts] || 0,
|
retry_attempts: example.metadata[:retry_attempts] || 0,
|
||||||
job_url: QA::Runtime::Env.ci_job_url,
|
job_url: QA::Runtime::Env.ci_job_url,
|
||||||
pipeline_url: env('CI_PIPELINE_URL'),
|
pipeline_url: env('CI_PIPELINE_URL'),
|
||||||
pipeline_id: env('CI_PIPELINE_ID')
|
pipeline_id: env('CI_PIPELINE_ID'),
|
||||||
|
testcase: example.metadata[:testcase]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|
|
@ -56,13 +56,14 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
||||||
retry_attempts: 0,
|
retry_attempts: 0,
|
||||||
job_url: ci_job_url,
|
job_url: ci_job_url,
|
||||||
pipeline_url: ci_pipeline_url,
|
pipeline_url: ci_pipeline_url,
|
||||||
pipeline_id: ci_pipeline_id
|
pipeline_id: ci_pipeline_id,
|
||||||
|
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_spec(&spec)
|
def run_spec(&spec)
|
||||||
spec ||= -> { it('spec') {} }
|
spec ||= -> { it('spec', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} }
|
||||||
|
|
||||||
describe_successfully('stats export', &spec).tap do |example_group|
|
describe_successfully('stats export', &spec).tap do |example_group|
|
||||||
example_group.examples.each { |ex| ex.metadata[:file_path] = file_path }
|
example_group.examples.each { |ex| ex.metadata[:file_path] = file_path }
|
||||||
|
@ -131,7 +132,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
||||||
|
|
||||||
it 'exports data to influxdb with correct reliable tag' do
|
it 'exports data to influxdb with correct reliable tag' do
|
||||||
run_spec do
|
run_spec do
|
||||||
it('spec', :reliable) {}
|
it('spec', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||||
|
@ -143,7 +144,7 @@ describe QA::Support::Formatters::TestStatsFormatter do
|
||||||
|
|
||||||
it 'exports data to influxdb with correct quarantine tag' do
|
it 'exports data to influxdb with correct quarantine tag' do
|
||||||
run_spec do
|
run_spec do
|
||||||
it('spec', :quarantine) {}
|
it('spec', :quarantine, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {}
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(influx_write_api).to have_received(:write).with(data: [data])
|
expect(influx_write_api).to have_received(:write).with(data: [data])
|
||||||
|
|
|
@ -3,15 +3,11 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_sessions do
|
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_sessions do
|
||||||
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
|
include SessionHelpers
|
||||||
|
|
||||||
context 'when session cookie is set' do
|
context 'when session cookie is set' do
|
||||||
before do
|
before do
|
||||||
Gitlab::Redis::Sessions.with do |redis|
|
stub_session(session_hash)
|
||||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
|
||||||
end
|
|
||||||
|
|
||||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is logged in' do
|
context 'when user is logged in' do
|
||||||
|
|
|
@ -94,13 +94,13 @@ describe('Todos', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates pending text', () => {
|
it('updates pending text', () => {
|
||||||
expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
|
expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
|
||||||
addDelimiter(TEST_COUNT_BIG),
|
addDelimiter(TEST_COUNT_BIG),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates done text', () => {
|
it('updates done text', () => {
|
||||||
expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
|
expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
|
||||||
addDelimiter(TEST_DONE_COUNT_BIG),
|
addDelimiter(TEST_DONE_COUNT_BIG),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,22 +3,22 @@
|
||||||
require "spec_helper"
|
require "spec_helper"
|
||||||
|
|
||||||
RSpec.describe Admin::BackgroundMigrationsHelper do
|
RSpec.describe Admin::BackgroundMigrationsHelper do
|
||||||
describe '#batched_migration_status_badge_class_name' do
|
describe '#batched_migration_status_badge_variant' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
where(:status, :class_name) do
|
where(:status, :variant) do
|
||||||
:active | 'badge-info'
|
:active | :info
|
||||||
:paused | 'badge-warning'
|
:paused | :warning
|
||||||
:failed | 'badge-danger'
|
:failed | :danger
|
||||||
:finished | 'badge-success'
|
:finished | :success
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { helper.batched_migration_status_badge_class_name(migration) }
|
subject { helper.batched_migration_status_badge_variant(migration) }
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
let(:migration) { build(:batched_background_migration, status: status) }
|
let(:migration) { build(:batched_background_migration, status: status) }
|
||||||
|
|
||||||
it { is_expected.to eq(class_name) }
|
it { is_expected.to eq(variant) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -38,20 +38,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do
|
||||||
expect(job.deployment.environment).to eq(job.persisted_environment)
|
expect(job.deployment.environment).to eq(job.persisted_environment)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when creation failure occures' do
|
|
||||||
before do
|
|
||||||
allow_next_instance_of(Deployment) do |deployment|
|
|
||||||
allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'trackes the exception' do
|
|
||||||
expect { subject }.to raise_error(described_class::DeploymentCreationError)
|
|
||||||
|
|
||||||
expect(Deployment.count).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the corresponding environment does not exist' do
|
context 'when the corresponding environment does not exist' do
|
||||||
let!(:environment) { }
|
let!(:environment) { }
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,19 @@ require 'spec_helper'
|
||||||
RSpec.describe Gitlab::RackAttack::Request do
|
RSpec.describe Gitlab::RackAttack::Request do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let(:env) { {} }
|
||||||
|
let(:session) { {} }
|
||||||
|
let(:request) do
|
||||||
|
::Rack::Attack::Request.new(
|
||||||
|
env.reverse_merge(
|
||||||
|
'REQUEST_METHOD' => 'GET',
|
||||||
|
'PATH_INFO' => path,
|
||||||
|
'rack.input' => StringIO.new,
|
||||||
|
'rack.session' => session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'FILES_PATH_REGEX' do
|
describe 'FILES_PATH_REGEX' do
|
||||||
subject { described_class::FILES_PATH_REGEX }
|
subject { described_class::FILES_PATH_REGEX }
|
||||||
|
|
||||||
|
@ -16,11 +29,63 @@ RSpec.describe Gitlab::RackAttack::Request do
|
||||||
it { is_expected.not_to match('/api/v4/projects/some/nested/repo/repository/files/README') }
|
it { is_expected.not_to match('/api/v4/projects/some/nested/repo/repository/files/README') }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#deprecated_api_request?' do
|
describe '#api_request?' do
|
||||||
let(:env) { { 'REQUEST_METHOD' => 'GET', 'rack.input' => StringIO.new, 'PATH_INFO' => path, 'QUERY_STRING' => query } }
|
subject { request.api_request? }
|
||||||
let(:request) { ::Rack::Attack::Request.new(env) }
|
|
||||||
|
|
||||||
subject { !!request.__send__(:deprecated_api_request?) }
|
where(:path, :env, :expected) do
|
||||||
|
'/' | {} | false
|
||||||
|
'/groups' | {} | false
|
||||||
|
|
||||||
|
'/api' | {} | true
|
||||||
|
'/api/v4/groups/1' | {} | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(expected) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#web_request?' do
|
||||||
|
subject { request.web_request? }
|
||||||
|
|
||||||
|
where(:path, :env, :expected) do
|
||||||
|
'/' | {} | true
|
||||||
|
'/groups' | {} | true
|
||||||
|
|
||||||
|
'/api' | {} | false
|
||||||
|
'/api/v4/groups/1' | {} | false
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(expected) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#frontend_request?', :allow_forgery_protection do
|
||||||
|
subject { request.send(:frontend_request?) }
|
||||||
|
|
||||||
|
let(:path) { '/' }
|
||||||
|
|
||||||
|
# Define these as local variables so we can use them in the `where` block.
|
||||||
|
valid_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
|
||||||
|
other_token = SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH)
|
||||||
|
|
||||||
|
where(:session, :env, :expected) do
|
||||||
|
{} | {} | false # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
||||||
|
{} | { 'HTTP_X_CSRF_TOKEN' => valid_token } | false
|
||||||
|
{ _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => other_token } | false
|
||||||
|
{ _csrf_token: valid_token } | { 'HTTP_X_CSRF_TOKEN' => valid_token } | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(expected) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#deprecated_api_request?' do
|
||||||
|
subject { request.send(:deprecated_api_request?) }
|
||||||
|
|
||||||
|
let(:env) { { 'QUERY_STRING' => query } }
|
||||||
|
|
||||||
where(:path, :query, :expected) do
|
where(:path, :query, :expected) do
|
||||||
'/' | '' | false
|
'/' | '' | false
|
||||||
|
|
|
@ -2064,6 +2064,31 @@ RSpec.describe Ci::Build do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'build auto retry feature' do
|
describe 'build auto retry feature' do
|
||||||
|
context 'with deployment job' do
|
||||||
|
let(:build) do
|
||||||
|
create(:ci_build, :deploy_to_production, :with_deployment,
|
||||||
|
user: user, pipeline: pipeline, project: project)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
allow(build).to receive(:auto_retry_allowed?) { true }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a deployment when a build is dropped' do
|
||||||
|
expect { build.drop!(:script_failure) }.to change { Deployment.count }.by(1)
|
||||||
|
|
||||||
|
retried_deployment = Deployment.last
|
||||||
|
expect(build.deployment.environment).to eq(retried_deployment.environment)
|
||||||
|
expect(build.deployment.ref).to eq(retried_deployment.ref)
|
||||||
|
expect(build.deployment.sha).to eq(retried_deployment.sha)
|
||||||
|
expect(build.deployment.tag).to eq(retried_deployment.tag)
|
||||||
|
expect(build.deployment.user).to eq(retried_deployment.user)
|
||||||
|
expect(build.deployment).to be_failed
|
||||||
|
expect(retried_deployment).to be_created
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#retries_count' do
|
describe '#retries_count' do
|
||||||
subject { create(:ci_build, name: 'test', pipeline: pipeline) }
|
subject { create(:ci_build, name: 'test', pipeline: pipeline) }
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'mime/types'
|
||||||
|
|
||||||
RSpec.describe API::Commits do
|
RSpec.describe API::Commits do
|
||||||
include ProjectForksHelper
|
include ProjectForksHelper
|
||||||
|
include SessionHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
|
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
|
||||||
|
@ -378,14 +379,7 @@ RSpec.describe API::Commits do
|
||||||
|
|
||||||
context 'when using warden' do
|
context 'when using warden' do
|
||||||
it 'increments usage counters', :clean_gitlab_redis_sessions do
|
it 'increments usage counters', :clean_gitlab_redis_sessions do
|
||||||
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
|
stub_session('warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]])
|
||||||
session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
|
|
||||||
|
|
||||||
Gitlab::Redis::Sessions.with do |redis|
|
|
||||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
|
||||||
end
|
|
||||||
|
|
||||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
|
||||||
|
|
||||||
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
|
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
|
||||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
|
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
|
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
|
||||||
include RackAttackSpecHelpers
|
include RackAttackSpecHelpers
|
||||||
|
include SessionHelpers
|
||||||
|
|
||||||
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
||||||
|
|
||||||
|
@ -63,6 +64,22 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'API requests from the frontend', :api, :clean_gitlab_redis_sessions do
|
||||||
|
context 'when unauthenticated' do
|
||||||
|
it_behaves_like 'rate-limited frontend API requests' do
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_unauthenticated' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authenticated' do
|
||||||
|
it_behaves_like 'rate-limited frontend API requests' do
|
||||||
|
let_it_be(:personal_access_token) { create(:personal_access_token) }
|
||||||
|
|
||||||
|
let(:throttle_setting_prefix) { 'throttle_authenticated' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'API requests authenticated with personal access token', :api do
|
describe 'API requests authenticated with personal access token', :api do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:token) { create(:personal_access_token, user: user) }
|
let_it_be(:token) { create(:personal_access_token, user: user) }
|
||||||
|
|
82
spec/services/deployments/create_for_build_service_spec.rb
Normal file
82
spec/services/deployments/create_for_build_service_spec.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Deployments::CreateForBuildService do
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:service) { described_class.new }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
subject { service.execute(build) }
|
||||||
|
|
||||||
|
context 'with a deployment job' do
|
||||||
|
let!(:build) { create(:ci_build, :start_review_app, project: project) }
|
||||||
|
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||||
|
|
||||||
|
it 'creates a deployment record' do
|
||||||
|
expect { subject }.to change { Deployment.count }.by(1)
|
||||||
|
|
||||||
|
build.reset
|
||||||
|
expect(build.deployment.project).to eq(build.project)
|
||||||
|
expect(build.deployment.ref).to eq(build.ref)
|
||||||
|
expect(build.deployment.sha).to eq(build.sha)
|
||||||
|
expect(build.deployment.deployable).to eq(build)
|
||||||
|
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
||||||
|
expect(build.deployment.environment).to eq(build.persisted_environment)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when creation failure occures' do
|
||||||
|
before do
|
||||||
|
allow(build).to receive(:create_deployment!) { raise ActiveRecord::RecordInvalid }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'trackes the exception' do
|
||||||
|
expect { subject }.to raise_error(described_class::DeploymentCreationError)
|
||||||
|
|
||||||
|
expect(Deployment.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the corresponding environment does not exist' do
|
||||||
|
let!(:environment) { }
|
||||||
|
|
||||||
|
it 'does not create a deployment record' do
|
||||||
|
expect { subject }.not_to change { Deployment.count }
|
||||||
|
|
||||||
|
expect(build.deployment).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a teardown job' do
|
||||||
|
let!(:build) { create(:ci_build, :stop_review_app, project: project) }
|
||||||
|
let!(:environment) { create(:environment, name: build.expanded_environment_name) }
|
||||||
|
|
||||||
|
it 'does not create a deployment record' do
|
||||||
|
expect { subject }.not_to change { Deployment.count }
|
||||||
|
|
||||||
|
expect(build.deployment).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a normal job' do
|
||||||
|
let!(:build) { create(:ci_build, project: project) }
|
||||||
|
|
||||||
|
it 'does not create a deployment record' do
|
||||||
|
expect { subject }.not_to change { Deployment.count }
|
||||||
|
|
||||||
|
expect(build.deployment).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a bridge' do
|
||||||
|
let!(:build) { create(:ci_bridge, project: project) }
|
||||||
|
|
||||||
|
it 'does not create a deployment record' do
|
||||||
|
expect { subject }.not_to change { Deployment.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -26,14 +26,14 @@ module RackAttackSpecHelpers
|
||||||
{ 'AUTHORIZATION' => "Basic #{encoded_login}" }
|
{ 'AUTHORIZATION' => "Basic #{encoded_login}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def expect_rejection(&block)
|
def expect_rejection(name = nil, &block)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:too_many_requests)
|
expect(response).to have_gitlab_http_status(:too_many_requests)
|
||||||
|
|
||||||
expect(response.headers.to_h).to include(
|
expect(response.headers.to_h).to include(
|
||||||
'RateLimit-Limit' => a_string_matching(/^\d+$/),
|
'RateLimit-Limit' => a_string_matching(/^\d+$/),
|
||||||
'RateLimit-Name' => a_string_matching(/^throttle_.*$/),
|
'RateLimit-Name' => name || a_string_matching(/^throttle_.*$/),
|
||||||
'RateLimit-Observed' => a_string_matching(/^\d+$/),
|
'RateLimit-Observed' => a_string_matching(/^\d+$/),
|
||||||
'RateLimit-Remaining' => a_string_matching(/^\d+$/),
|
'RateLimit-Remaining' => a_string_matching(/^\d+$/),
|
||||||
'Retry-After' => a_string_matching(/^\d+$/)
|
'Retry-After' => a_string_matching(/^\d+$/)
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SessionHelpers
|
module SessionHelpers
|
||||||
|
# Stub a session in Redis, for use in request specs where we can't mock the session directly.
|
||||||
|
# This also needs the :clean_gitlab_redis_sessions tag on the spec.
|
||||||
|
def stub_session(session_hash)
|
||||||
|
unless RSpec.current_example.metadata[:clean_gitlab_redis_sessions]
|
||||||
|
raise 'Add :clean_gitlab_redis_sessions to your spec!'
|
||||||
|
end
|
||||||
|
|
||||||
|
session_id = Rack::Session::SessionId.new(SecureRandom.hex)
|
||||||
|
|
||||||
|
Gitlab::Redis::Sessions.with do |redis|
|
||||||
|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
||||||
|
end
|
||||||
|
|
||||||
|
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
||||||
|
end
|
||||||
|
|
||||||
def expect_single_session_with_authenticated_ttl
|
def expect_single_session_with_authenticated_ttl
|
||||||
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
|
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@ RSpec.shared_examples 'when the snippet is not found' do
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.shared_examples 'snippet edit usage data counters' do
|
RSpec.shared_examples 'snippet edit usage data counters' do
|
||||||
|
include SessionHelpers
|
||||||
|
|
||||||
context 'when user is sessionless' do
|
context 'when user is sessionless' do
|
||||||
it 'does not track usage data actions' do
|
it 'does not track usage data actions' do
|
||||||
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
|
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
|
||||||
|
@ -20,14 +22,7 @@ RSpec.shared_examples 'snippet edit usage data counters' do
|
||||||
|
|
||||||
context 'when user is not sessionless', :clean_gitlab_redis_sessions do
|
context 'when user is not sessionless', :clean_gitlab_redis_sessions do
|
||||||
before do
|
before do
|
||||||
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
|
stub_session('warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]])
|
||||||
session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
|
|
||||||
|
|
||||||
Gitlab::Redis::Sessions.with do |redis|
|
|
||||||
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
|
|
||||||
end
|
|
||||||
|
|
||||||
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tracks usage data actions', :clean_gitlab_redis_sessions do
|
it 'tracks usage data actions', :clean_gitlab_redis_sessions do
|
||||||
|
|
|
@ -580,3 +580,88 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Requires let variables:
|
||||||
|
# * throttle_setting_prefix: "throttle_authenticated", "throttle_unauthenticated"
|
||||||
|
RSpec.shared_examples 'rate-limited frontend API requests' do
|
||||||
|
let(:requests_per_period) { 1 }
|
||||||
|
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
|
||||||
|
let(:csrf_session) { { _csrf_token: csrf_token } }
|
||||||
|
let(:personal_access_token) { nil }
|
||||||
|
|
||||||
|
let(:api_path) { '/projects' }
|
||||||
|
|
||||||
|
# These don't actually exist, so a 404 is the expected response.
|
||||||
|
let(:files_api_path) { '/projects/1/repository/files/ref/path' }
|
||||||
|
let(:packages_api_path) { '/projects/1/packages/foo' }
|
||||||
|
let(:deprecated_api_path) { '/groups/1?with_projects=true' }
|
||||||
|
|
||||||
|
def get_api(path: api_path, csrf: false)
|
||||||
|
headers = csrf ? { 'X-CSRF-Token' => csrf_token } : nil
|
||||||
|
get api(path, personal_access_token: personal_access_token), headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_not_found(&block)
|
||||||
|
yield
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_application_setting(
|
||||||
|
"#{throttle_setting_prefix}_enabled" => true,
|
||||||
|
"#{throttle_setting_prefix}_requests_per_period" => requests_per_period,
|
||||||
|
"#{throttle_setting_prefix}_api_enabled" => true,
|
||||||
|
"#{throttle_setting_prefix}_api_requests_per_period" => requests_per_period,
|
||||||
|
"#{throttle_setting_prefix}_web_enabled" => true,
|
||||||
|
"#{throttle_setting_prefix}_web_requests_per_period" => requests_per_period,
|
||||||
|
"#{throttle_setting_prefix}_files_api_enabled" => true,
|
||||||
|
"#{throttle_setting_prefix}_packages_api_enabled" => true,
|
||||||
|
"#{throttle_setting_prefix}_deprecated_api_enabled" => true
|
||||||
|
)
|
||||||
|
|
||||||
|
stub_session(csrf_session)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a CSRF token' do
|
||||||
|
it 'uses the rate limit for web requests' do
|
||||||
|
requests_per_period.times { get_api csrf: true }
|
||||||
|
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true }
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: files_api_path }
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: packages_api_path }
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_web") { get_api csrf: true, path: deprecated_api_path }
|
||||||
|
|
||||||
|
# API rate limit is not triggered yet
|
||||||
|
expect_ok { get_api }
|
||||||
|
expect_not_found { get_api path: files_api_path }
|
||||||
|
expect_not_found { get_api path: packages_api_path }
|
||||||
|
expect_not_found { get_api path: deprecated_api_path }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a CSRF session' do
|
||||||
|
let(:csrf_session) { nil }
|
||||||
|
|
||||||
|
it 'always uses the rate limit for API requests' do
|
||||||
|
requests_per_period.times { get_api csrf: true }
|
||||||
|
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_api") { get_api csrf: true }
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_api") { get_api }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a CSRF token' do
|
||||||
|
it 'uses the rate limit for API requests' do
|
||||||
|
requests_per_period.times { get_api }
|
||||||
|
|
||||||
|
expect_rejection("#{throttle_setting_prefix}_api") { get_api }
|
||||||
|
|
||||||
|
# Web and custom API rate limits are not triggered yet
|
||||||
|
expect_ok { get_api csrf: true }
|
||||||
|
expect_not_found { get_api path: files_api_path }
|
||||||
|
expect_not_found { get_api path: packages_api_path }
|
||||||
|
expect_not_found { get_api path: deprecated_api_path }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in a new issue