Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/rspec/before_all_role_assignment.yml1
-rw-r--r--.rubocop_todo/style/inline_disable_annotation.yml1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/api/bulk_imports_api.js14
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue2
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/actions.js2
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js (renamed from app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js)0
-rw-r--r--app/assets/javascripts/contextual_sidebar.js112
-rw-r--r--app/assets/javascripts/fly_out_nav.js205
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue31
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue16
-rw-r--r--app/assets/javascripts/layout_nav.js8
-rw-r--r--app/assets/javascripts/organizations/constants.js2
-rw-r--r--app/assets/javascripts/organizations/users/components/app.vue43
-rw-r--r--app/assets/javascripts/organizations/users/components/users_view.vue24
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql15
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js2
-rw-r--r--app/graphql/resolvers/ci/catalog/resources/versions_resolver.rb25
-rw-r--r--app/graphql/resolvers/ci/catalog/versions_resolver.rb24
-rw-r--r--app/graphql/types/ci/catalog/resource_type.rb16
-rw-r--r--app/graphql/types/ci/catalog/resources/version_sort_enum.rb14
-rw-r--r--app/graphql/types/ci/catalog/resources/version_type.rb50
-rw-r--r--app/models/ci/bridge.rb12
-rw-r--r--app/models/ci/catalog/resources/version.rb10
-rw-r--r--app/services/ml/model_versions/delete_service.rb32
-rw-r--r--app/validators/json_schemas/scan_result_policy_project_approval_settings.json2
-rw-r--r--config/metrics/counts_28d/20210216175055_merge_requests.yml3
-rw-r--r--config/metrics/counts_28d/20210216175542_ci_builds.yml3
-rw-r--r--config/metrics/counts_28d/20210216181148_service_desk_issues.yml3
-rw-r--r--config/metrics/counts_28d/20210216183730_jira.yml3
-rw-r--r--config/metrics/counts_28d/20211126091206_p_analytics_ci_cd_lead_time_monthly.yml3
-rw-r--r--config/metrics/counts_all/20210216175229_auto_devops_enabled.yml3
-rw-r--r--config/metrics/counts_all/20210216175514_ci_external_pipelines.yml3
-rw-r--r--config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml3
-rw-r--r--config/metrics/counts_all/20210216181011_projects_with_packages.yml3
-rw-r--r--config/metrics/counts_all/20210216181102_issues.yml3
-rw-r--r--config/metrics/counts_all/20210216181254_projects.yml3
-rw-r--r--config/metrics/counts_all/20210216182002_remote_mirrors.yml3
-rw-r--r--db/docs/audit_events_instance_amazon_s3_configurations.yml10
-rw-r--r--db/migrate/20231107140642_create_audit_events_instance_amazon_s3_configurations.rb24
-rw-r--r--db/schema_migrations/202311071406421
-rw-r--r--db/structure.sql34
-rw-r--r--doc/api/graphql/reference/index.md61
-rw-r--r--doc/architecture/blueprints/gitlab_steps/data.drawio.pngbin0 -> 42192 bytes
-rw-r--r--doc/architecture/blueprints/gitlab_steps/implementation.md350
-rw-r--r--doc/architecture/blueprints/gitlab_steps/index.md92
-rw-r--r--doc/architecture/blueprints/gitlab_steps/runner-integration.md116
-rw-r--r--doc/architecture/blueprints/gitlab_steps/step-runner-sequence.drawio.pngbin0 -> 70107 bytes
-rw-r--r--doc/user/application_security/policies/scan-result-policies.md5
-rw-r--r--doc/user/group/manage.md5
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb2
-rw-r--r--lib/gitlab/redis/wrapper.rb10
-rw-r--r--locale/gitlab.pot4
-rw-r--r--spec/factories/deployments.rb4
-rw-r--r--spec/finders/ci/catalog/resources/versions_finder_spec.rb23
-rw-r--r--spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js (renamed from spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js)2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_history_link_spec.js34
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js18
-rw-r--r--spec/frontend/organizations/users/components/app_spec.js68
-rw-r--r--spec/frontend/organizations/users/components/users_view_spec.js28
-rw-r--r--spec/frontend/organizations/users/mock_data.js8
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js99
-rw-r--r--spec/frontend_integration/fly_out_nav_browser_spec.js366
-rw-r--r--spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb69
-rw-r--r--spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb66
-rw-r--r--spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/catalog/resources/version_type_spec.rb21
-rw-r--r--spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb4
-rw-r--r--spec/models/ci/bridge_spec.rb18
-rw-r--r--spec/models/ci/catalog/resources/version_spec.rb27
-rw-r--r--spec/requests/api/deployments_spec.rb52
-rw-r--r--spec/requests/api/environments_spec.rb67
-rw-r--r--spec/requests/api/graphql/ci/catalog/resource_spec.rb25
-rw-r--r--spec/requests/api/graphql/ci/catalog/resources_spec.rb21
-rw-r--r--spec/services/ml/model_versions/delete_service_spec.rb55
-rw-r--r--spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb2
-rw-r--r--spec/support/shared_examples/services/protected_branches_shared_examples.rb2
80 files changed, 1584 insertions, 957 deletions
diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml
index 9f85e031775..5ab492b4a29 100644
--- a/.rubocop_todo/rspec/before_all_role_assignment.yml
+++ b/.rubocop_todo/rspec/before_all_role_assignment.yml
@@ -223,7 +223,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/graphql/resolvers/boards/epic_lists_resolvers_spec.rb'
- 'ee/spec/graphql/resolvers/ci/catalog/resource_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb'
- - 'ee/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb'
- 'ee/spec/graphql/resolvers/dast_site_validation_resolver_spec.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index faa79f6aa3a..ae0d5084358 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -2938,7 +2938,6 @@ Style/InlineDisableAnnotation:
- 'spec/graphql/mutations/design_management/delete_spec.rb'
- 'spec/graphql/resolvers/board_resolver_spec.rb'
- 'spec/graphql/resolvers/boards_resolver_spec.rb'
- - 'spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb'
- 'spec/haml_lint/linter/inline_javascript_spec.rb'
- 'spec/haml_lint/linter/no_plain_nodes_spec.rb'
- 'spec/helpers/admin/abuse_reports_helper_spec.rb'
diff --git a/Gemfile b/Gemfile
index 36a24eb099b..de7a2897594 100644
--- a/Gemfile
+++ b/Gemfile
@@ -504,7 +504,7 @@ group :test do
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'gitlab_quality-test_tooling', '~> 1.5.2', require: false, feature_category: :tooling
+ gem 'gitlab_quality-test_tooling', '~> 1.5.3', require: false, feature_category: :tooling
end
gem 'octokit', '~> 6.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index c04ec4641c3..124843b3032 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -221,7 +221,7 @@
{"name":"gitlab-styles","version":"11.0.0","platform":"ruby","checksum":"0dd8ec066ce9955ac51d3616c6bfded30f75bb526f39ff392ece6f43d5b9406b"},
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
-{"name":"gitlab_quality-test_tooling","version":"1.5.2","platform":"ruby","checksum":"1fe87d513f005fa2ad6c35ca2bef10a262e6ab3cca3754ff5ccf121622eb45f9"},
+{"name":"gitlab_quality-test_tooling","version":"1.5.3","platform":"ruby","checksum":"d53f3d6319d8948091caefa9cd0d1e71719df4f26a3a9d4936458ffcb72bba30"},
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1b9ebfe61b9..e348606bf90 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -716,7 +716,7 @@ GEM
omniauth (>= 1.3, < 3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
- gitlab_quality-test_tooling (1.5.2)
+ gitlab_quality-test_tooling (1.5.3)
activesupport (>= 6.1, < 7.2)
amatch (~> 0.4.1)
gitlab (~> 4.19)
@@ -1886,7 +1886,7 @@ DEPENDENCIES
gitlab-utils!
gitlab_chronic_duration (~> 0.12)
gitlab_omniauth-ldap (~> 2.2.0)
- gitlab_quality-test_tooling (~> 1.5.2)
+ gitlab_quality-test_tooling (~> 1.5.3)
gon (~> 6.4.0)
google-apis-androidpublisher_v3 (~> 0.34.0)
google-apis-cloudbilling_v1 (~> 0.21.0)
diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js
index 248f5601705..a5c9f904c1f 100644
--- a/app/assets/javascripts/api/bulk_imports_api.js
+++ b/app/assets/javascripts/api/bulk_imports_api.js
@@ -1,12 +1,22 @@
import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
-const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
+const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/:id/entities';
+const BULK_IMPORTS_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
const BULK_IMPORT_ENTITIES_FAILURES_PATH =
'/api/:version/bulk_imports/:id/entities/:entity_id/failures';
+export const getBulkImportHistory = (id, params = {}) => {
+ const bulkImportHistoryUrl = buildApiUrl(BULK_IMPORT_ENTITIES_PATH).replace(
+ ':id',
+ encodeURIComponent(id),
+ );
+
+ return axios.get(bulkImportHistoryUrl, { params });
+};
+
export const getBulkImportsHistory = (params) =>
- axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params });
+ axios.get(buildApiUrl(BULK_IMPORTS_ENTITIES_PATH), { params });
export const getBulkImportFailures = (id, entityId, { page, perPage }) => {
const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH)
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index 4fded3aec60..c7fe9724485 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -38,7 +38,7 @@ export default {
},
tooltipConfig: {
boundary: 'viewport',
- placement: 'bottom',
+ placement: 'top',
customClass: 'gl-pointer-events-none',
},
components: {
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
index 04aca11b945..5247faef363 100644
--- a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
@@ -1,7 +1,7 @@
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import { STATUS_NOT_FOUND } from '../../constants';
+import { parseCodeclimateMetrics } from '../utils/codequality_parser';
import * as types from './mutation_types';
-import { parseCodeclimateMetrics } from './utils/codequality_parser';
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
index 417297df43c..417297df43c 100644
--- a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
deleted file mode 100644
index ab5f01227fb..00000000000
--- a/app/assets/javascripts/contextual_sidebar.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
-import $ from 'jquery';
-import { debounce } from 'lodash';
-import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
-
-export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
-
-export default class ContextualSidebar {
- constructor() {
- this.initDomElements();
- this.render();
- }
-
- initDomElements() {
- this.$page = $('.layout-page');
- this.$sidebar = $('.nav-sidebar');
-
- if (!this.$sidebar.length) return;
-
- this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
- this.$overlay = $('.mobile-overlay');
- this.$openSidebar = $('.toggle-mobile-nav');
- this.$closeSidebar = $('.close-nav-button');
- this.$sidebarToggle = $('.js-toggle-sidebar');
- }
-
- bindEvents() {
- if (!this.$sidebar.length) return;
-
- this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
- this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
- this.$overlay.on('click', () => this.toggleSidebarNav(false));
- this.$sidebarToggle.on('click', () => {
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- this.toggleSidebarNav(!this.$sidebar.hasClass('sidebar-expanded-mobile'));
- } else {
- const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
- this.toggleCollapsedSidebar(value, true);
- }
- });
-
- $(window).on(
- 'resize',
- debounce(() => this.render(), 100),
- );
- }
-
- // See documentation: https://design.gitlab.com/regions/navigation#contextual-navigation
- // NOTE: at 1200px nav sidebar should not overlap the content
- // https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24555#note_134136110
- static isDesktopBreakpoint = () => bp.windowWidth() >= breakpoints.xl;
- static setCollapsedCookie(value) {
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- return;
- }
- setCookie('sidebar_collapsed', value, { expires: 365 * 10 });
- }
-
- toggleSidebarNav(show) {
- const breakpoint = bp.getBreakpointSize();
- const dbp = ContextualSidebar.isDesktopBreakpoint();
- const supportedSizes = ['xs', 'sm', 'md'];
-
- this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
- this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
- this.$overlay.toggleClass(
- 'mobile-nav-open',
- supportedSizes.includes(breakpoint) ? show : false,
- );
- this.$sidebar.removeClass('sidebar-collapsed-desktop');
- }
-
- toggleCollapsedSidebar(collapsed, saveCookie) {
- const breakpoint = bp.getBreakpointSize();
- const dbp = ContextualSidebar.isDesktopBreakpoint();
- const supportedSizes = ['xs', 'sm', 'md'];
-
- if (this.$sidebar.length) {
- this.$sidebar.toggleClass(`sidebar-collapsed-desktop ${SIDEBAR_COLLAPSED_CLASS}`, collapsed);
- this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
- this.$page.toggleClass(
- 'page-with-icon-sidebar',
- supportedSizes.includes(breakpoint) ? true : collapsed,
- );
- }
-
- if (saveCookie) {
- ContextualSidebar.setCollapsedCookie(collapsed);
- }
-
- requestIdleCallback(() => this.toggleSidebarOverflow());
- }
-
- toggleSidebarOverflow() {
- if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
- this.$innerScroll.css('overflow-y', 'scroll');
- } else {
- this.$innerScroll.css('overflow-y', '');
- }
- }
-
- render() {
- if (!this.$sidebar.length) return;
-
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- this.toggleSidebarNav(false);
- } else {
- const collapse = parseBoolean(getCookie('sidebar_collapsed'));
- this.toggleCollapsedSidebar(collapse, true);
- }
- }
-}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
deleted file mode 100644
index 0fb70fb831e..00000000000
--- a/app/assets/javascripts/fly_out_nav.js
+++ /dev/null
@@ -1,205 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
-
-const HIDE_INTERVAL_TIMEOUT = 300;
-const COLLAPSED_PANEL_WIDTH = 48;
-const IS_OVER_CLASS = 'is-over';
-const IS_ABOVE_CLASS = 'is-above';
-const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
-let currentOpenMenu = null;
-let menuCornerLocs;
-let timeoutId;
-let sidebar;
-
-export const mousePos = [];
-
-export const setSidebar = (el) => {
- sidebar = el;
-};
-export const getOpenMenu = () => currentOpenMenu;
-export const setOpenMenu = (menu = null) => {
- currentOpenMenu = menu;
-};
-
-export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
-
-export const getHeaderHeight = () => sidebar?.offsetTop || 0;
-
-export const isSidebarCollapsed = () =>
- sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS);
-
-export const canShowActiveSubItems = (el) => {
- if (el.classList.contains('active') && !isSidebarCollapsed()) {
- return false;
- }
-
- return true;
-};
-
-export const canShowSubItems = () => ['md', 'lg', 'xl'].includes(bp.getBreakpointSize());
-
-export const getHideSubItemsInterval = () => {
- if (!currentOpenMenu || !mousePos.length) return 0;
-
- const currentMousePos = mousePos[mousePos.length - 1];
- const prevMousePos = mousePos[0];
- const currentMousePosY = currentMousePos.y;
- const [menuTop, menuBottom] = menuCornerLocs;
-
- if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
-
- if (
- slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
- slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
- ) {
- return HIDE_INTERVAL_TIMEOUT;
- }
-
- return 0;
-};
-
-export const calculateTop = (boundingRect, outerHeight) => {
- const windowHeight = window.innerHeight;
- const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
-
- return bottomOverflow < 0
- ? boundingRect.top - outerHeight + boundingRect.height
- : boundingRect.top;
-};
-
-export const hideMenu = (el) => {
- if (!el) return;
-
- const parentEl = el.parentNode;
-
- el.style.display = '';
- el.style.transform = '';
- el.classList.remove(IS_ABOVE_CLASS);
- el.classList.remove('fly-out-list');
- parentEl.classList.remove(IS_OVER_CLASS);
- parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
-
- setOpenMenu();
-};
-
-export const moveSubItemsToPosition = (el, subItems) => {
- const hasSubItems = subItems.parentNode.querySelector('.has-sub-items');
- const header = subItems.querySelector('.fly-out-top-item');
- const boundingRect = el.getBoundingClientRect();
- const left = sidebar ? sidebar.offsetWidth : COLLAPSED_PANEL_WIDTH;
- let top = calculateTop(boundingRect, subItems.offsetHeight);
- const isAbove = top < boundingRect.top;
- if (hasSubItems) {
- top = isAbove ? top : top - header.offsetHeight;
- } else {
- top = boundingRect.top;
- }
-
- subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - getHeaderHeight()}px, 0)`; // eslint-disable-line no-param-reassign
- const subItemsRect = subItems.getBoundingClientRect();
-
- menuCornerLocs = [
- {
- x: subItemsRect.left, // left position of the sub items
- y: subItemsRect.top, // top position of the sub items
- },
- {
- x: subItemsRect.left, // left position of the sub items
- y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
- },
- ];
-
- if (isAbove) {
- subItems.classList.add(IS_ABOVE_CLASS);
- }
-};
-
-export const showSubLevelItems = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
- const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
-
- if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
-
- el.classList.add(IS_OVER_CLASS);
-
- if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
-
- subItems.style.display = 'block';
- el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
-
- setOpenMenu(subItems);
- moveSubItemsToPosition(el, subItems);
-};
-
-export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
-
- showSubLevelItems(el);
- }, timeout);
-};
-
-export const mouseLeaveTopItem = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
-
- if (
- !canShowSubItems() ||
- !canShowActiveSubItems(el) ||
- (subItems && subItems === currentOpenMenu)
- )
- return;
-
- el.classList.remove(IS_OVER_CLASS);
-};
-
-export const documentMouseMove = (e) => {
- mousePos.push({
- x: e.clientX,
- y: e.clientY,
- });
-
- if (mousePos.length > 6) mousePos.shift();
-};
-
-export const subItemsMouseLeave = (relatedTarget) => {
- clearTimeout(timeoutId);
-
- if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
- hideMenu(currentOpenMenu);
- }
-};
-
-export default () => {
- sidebar = document.querySelector('.nav-sidebar');
-
- if (!sidebar) return;
-
- const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
-
- const topItems = sidebar.querySelector('.sidebar-top-level-items');
- if (topItems) {
- sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
- }, getHideSubItemsInterval());
- });
- }
-
- items.forEach((el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
-
- if (subItems) {
- subItems.addEventListener('mouseleave', (e) => subItemsMouseLeave(e.relatedTarget));
- }
-
- el.addEventListener('mouseenter', (e) => mouseEnterTopItems(e.currentTarget));
- el.addEventListener('mouseleave', (e) => mouseLeaveTopItem(e.currentTarget));
- });
-
- document.addEventListener('mousemove', documentMouseMove);
-};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue b/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue
new file mode 100644
index 00000000000..218e7dee953
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlLink,
+ },
+
+ props: {
+ id: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ historyPath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ historyPathWithId() {
+ return mergeUrlParams({ bulk_import_id: this.id }, this.historyPath);
+ },
+ },
+};
+</script>
+<template>
+ <gl-link :href="historyPathWithId">{{ __('View details') }}</gl-link>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index c5f83bf90da..82faf4fc110 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -13,7 +13,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, isNumber } from 'lodash';
import { createAlert } from '~/alert';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__, __, n__, sprintf } from '~/locale';
@@ -36,6 +36,7 @@ import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants';
import { StatusPoller } from '../services/status_poller';
import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils';
import ImportActionsCell from './import_actions_cell.vue';
+import ImportHistoryLink from './import_history_link.vue';
import ImportSourceCell from './import_source_cell.vue';
import ImportStatusCell from './import_status.vue';
import ImportTargetCell from './import_target_cell.vue';
@@ -61,6 +62,7 @@ export default {
ImportTargetCell,
ImportStatusCell,
ImportActionsCell,
+ ImportHistoryLink,
PaginationBar,
HelpPopover,
},
@@ -352,6 +354,12 @@ export default {
return group.progress?.hasFailures;
},
+ showHistoryLink(group) {
+ // We need to check for `isNumber` to make sure `id` is passed from the backend
+ // and not "LOCAL-PROGRESS-${id}" as defined by client_factory.js
+ return group.progress?.id && isNumber(group.progress.id);
+ },
+
updateImportTarget(group, changes) {
const newImportTarget = {
...group.importTarget,
@@ -800,6 +808,12 @@ export default {
</template>
<template #cell(progress)="{ item: group }">
<import-status-cell :status="group.visibleStatus" :has-failures="hasFailures(group)" />
+ <import-history-link
+ v-if="showHistoryLink(group)"
+ :id="group.progress.id"
+ :history-path="historyPath"
+ class="gl-display-inline-block gl-mt-2"
+ />
</template>
<template #cell(actions)="{ item: group, index }">
<import-actions-cell
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 670170ec9b9..2cb8c37f192 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,6 +1,4 @@
import $ from 'jquery';
-import ContextualSidebar from './contextual_sidebar';
-import initFlyOutNav from './fly_out_nav';
import { setNotification } from './whats_new/utils/notification';
function hideEndFade($scrollingTabs) {
@@ -113,11 +111,5 @@ function initDeferred() {
}
export default function initLayoutNav() {
- if (!gon.use_new_navigation) {
- const contextualSidebar = new ContextualSidebar();
- contextualSidebar.bindEvents();
- initFlyOutNav();
- }
-
requestIdleCallback(initDeferred);
}
diff --git a/app/assets/javascripts/organizations/constants.js b/app/assets/javascripts/organizations/constants.js
index 8ade37b169e..d3da072b38d 100644
--- a/app/assets/javascripts/organizations/constants.js
+++ b/app/assets/javascripts/organizations/constants.js
@@ -2,3 +2,5 @@ export const RESOURCE_TYPE_GROUPS = 'groups';
export const RESOURCE_TYPE_PROJECTS = 'projects';
export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
+
+export const ORGANIZATION_USERS_PER_PAGE = 20;
diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue
index 1d5ea895ff0..065a1e004f2 100644
--- a/app/assets/javascripts/organizations/users/components/app.vue
+++ b/app/assets/javascripts/organizations/users/components/app.vue
@@ -1,9 +1,17 @@
<script>
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
+import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import organizationUsersQuery from '../graphql/organization_users.query.graphql';
import UsersView from './users_view.vue';
+const defaultPagination = {
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ before: '',
+ after: '',
+};
+
export default {
name: 'OrganizationsUsersApp',
components: {
@@ -20,16 +28,29 @@ export default {
data() {
return {
users: [],
+ pagination: {
+ ...defaultPagination,
+ },
+ pageInfo: {},
};
},
apollo: {
users: {
query: organizationUsersQuery,
variables() {
- return { id: this.organizationGid };
+ return {
+ id: this.organizationGid,
+ first: this.pagination.first,
+ last: this.pagination.last,
+ before: this.pagination.before,
+ after: this.pagination.after,
+ };
},
update(data) {
- return data.organization.organizationUsers.nodes.map(({ badges, user }) => {
+ const { nodes, pageInfo } = data.organization.organizationUsers;
+ this.pageInfo = pageInfo;
+
+ return nodes.map(({ badges, user }) => {
return { ...user, badges, email: user.publicEmail };
});
},
@@ -43,12 +64,28 @@ export default {
return this.$apollo.queries.users.loading;
},
},
+ methods: {
+ handlePrevPage() {
+ this.pagination.before = this.pageInfo.startCursor;
+ this.pagination.after = '';
+ },
+ handleNextPage() {
+ this.pagination.before = '';
+ this.pagination.after = this.pageInfo.endCursor;
+ },
+ },
};
</script>
<template>
<section>
<h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1>
- <users-view :users="users" :loading="loading" />
+ <users-view
+ :users="users"
+ :loading="loading"
+ :page-info="pageInfo"
+ @prev="handlePrevPage"
+ @next="handleNextPage"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/organizations/users/components/users_view.vue b/app/assets/javascripts/organizations/users/components/users_view.vue
index fac353bdaf6..c1f411fb958 100644
--- a/app/assets/javascripts/organizations/users/components/users_view.vue
+++ b/app/assets/javascripts/organizations/users/components/users_view.vue
@@ -1,11 +1,12 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
export default {
name: 'UsersView',
components: {
GlLoadingIcon,
+ GlKeysetPagination,
UsersTable,
},
inject: ['paths'],
@@ -15,6 +16,10 @@ export default {
required: false,
default: () => [],
},
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
loading: {
type: Boolean,
required: false,
@@ -25,6 +30,19 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
- <users-table v-else :users="users" :admin-user-path="paths.adminUser" />
+ <div>
+ <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
+ <template v-else>
+ <users-table :users="users" :admin-user-path="paths.adminUser" />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="__('Previous')"
+ :next-text="__('Next')"
+ @prev="$emit('prev')"
+ @next="$emit('next')"
+ />
+ </div>
+ </template>
+ </div>
</template>
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
index d98ebf9cd26..0b9b1314fa2 100644
--- a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -1,7 +1,15 @@
-query getOrganizationUsers($id: OrganizationsOrganizationID!) {
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getOrganizationUsers(
+ $id: OrganizationsOrganizationID!
+ $first: Int
+ $last: Int
+ $before: String!
+ $after: String!
+) {
organization(id: $id) {
id
- organizationUsers {
+ organizationUsers(first: $first, last: $last, before: $before, after: $after) {
nodes {
badges {
text
@@ -18,6 +26,9 @@ query getOrganizationUsers($id: OrganizationsOrganizationID!) {
lastActivityOn
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index e912bfa4f92..23d6fa64c64 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -8,12 +8,13 @@ import {
GlTableLite,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
+import { isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { getBulkImportsHistory } from '~/rest_api';
+import { joinPaths, getParameterValues } from '~/lib/utils/url_utility';
+import { getBulkImportHistory, getBulkImportsHistory } from '~/rest_api';
import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
@@ -110,12 +111,18 @@ export default {
showDetailsLink() {
return this.glFeatures.bulkImportDetailsPage;
},
+
+ paginationConfigCopy() {
+ return { ...this.paginationConfig };
+ },
},
watch: {
- paginationConfig: {
- handler() {
- this.loadHistoryItems();
+ paginationConfigCopy: {
+ handler(newValue, oldValue) {
+ if (!isEqual(newValue, oldValue)) {
+ this.loadHistoryItems();
+ }
},
deep: true,
},
@@ -159,10 +166,19 @@ export default {
},
methods: {
+ fetchFn(params) {
+ const bulkImportId = getParameterValues('bulk_import_id')[0];
+
+ return bulkImportId
+ ? getBulkImportHistory(bulkImportId, params)
+ : getBulkImportsHistory(params);
+ },
+
async loadHistoryItems() {
try {
this.loading = true;
- const { data: historyItems, headers } = await getBulkImportsHistory({
+
+ const { data: historyItems, headers } = await this.fetchFn({
page: this.paginationConfig.page,
per_page: this.paginationConfig.perPage,
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index 3af984dcf6c..479f9eae820 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -2,7 +2,7 @@ import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
-import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { i18n, codeQualityPrefixes } from './constants';
diff --git a/app/graphql/resolvers/ci/catalog/resources/versions_resolver.rb b/app/graphql/resolvers/ci/catalog/resources/versions_resolver.rb
new file mode 100644
index 00000000000..9332076a493
--- /dev/null
+++ b/app/graphql/resolvers/ci/catalog/resources/versions_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ module Catalog
+ module Resources
+ class VersionsResolver < BaseResolver
+ type Types::Ci::Catalog::Resources::VersionType.connection_type, null: true
+
+ # This allows a maximum of 1 call to the field that uses this resolver. If the
+ # field is evaluated on more than one node, it causes performance degradation.
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+
+ argument :sort, Types::Ci::Catalog::Resources::VersionSortEnum,
+ required: false,
+ description: 'Sort versions by given criteria.'
+
+ def resolve(sort: nil)
+ ::Ci::Catalog::Resources::VersionsFinder.new(object, current_user, sort: sort).execute
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/catalog/versions_resolver.rb b/app/graphql/resolvers/ci/catalog/versions_resolver.rb
deleted file mode 100644
index 046adeb7a67..00000000000
--- a/app/graphql/resolvers/ci/catalog/versions_resolver.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- module Ci
- module Catalog
- class VersionsResolver < ::Resolvers::ReleasesResolver
- type Types::ReleaseType.connection_type, null: true
-
- # This allows a maximum of 1 call to the field that uses this resolver. If the
- # field is evaluated on more than one node, it causes performance degradation.
- extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
-
- private
-
- def get_project
- object.respond_to?(:project) ? object.project : object
- end
-
- # Override the aliased method in ReleasesResolver
- alias_method :project, :get_project
- end
- end
- end
-end
diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb
index 918f7067211..c20106ef7c4 100644
--- a/app/graphql/types/ci/catalog/resource_type.rb
+++ b/app/graphql/types/ci/catalog/resource_type.rb
@@ -32,13 +32,14 @@ module Types
field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the catalog resource.',
alpha: { milestone: '16.1' }
- field :versions, Types::ReleaseType.connection_type, null: true,
+ field :versions, Types::Ci::Catalog::Resources::VersionType.connection_type, null: true,
description: 'Versions of the catalog resource. This field can only be ' \
'resolved for one catalog resource in any single request.',
- resolver: Resolvers::Ci::Catalog::VersionsResolver,
+ resolver: Resolvers::Ci::Catalog::Resources::VersionsResolver,
alpha: { milestone: '16.2' }
- field :latest_version, Types::ReleaseType, null: true, description: 'Latest version of the catalog resource.',
+ field :latest_version, Types::Ci::Catalog::Resources::VersionType, null: true,
+ description: 'Latest version of the catalog resource.',
alpha: { milestone: '16.1' }
field :latest_released_at, Types::TimeType, null: true,
@@ -69,11 +70,12 @@ module Types
end
def latest_version
- BatchLoader::GraphQL.for(object.project).batch do |projects, loader|
- latest_releases = ReleasesFinder.new(projects, current_user, latest: true).execute
+ BatchLoader::GraphQL.for(object).batch do |catalog_resources, loader|
+ latest_versions = ::Ci::Catalog::Resources::VersionsFinder.new(
+ catalog_resources, current_user, latest: true).execute
- latest_releases.index_by(&:project).each do |project, latest_release|
- loader.call(project, latest_release)
+ latest_versions.index_by(&:catalog_resource).each do |catalog_resource, latest_version|
+ loader.call(catalog_resource, latest_version)
end
end
end
diff --git a/app/graphql/types/ci/catalog/resources/version_sort_enum.rb b/app/graphql/types/ci/catalog/resources/version_sort_enum.rb
new file mode 100644
index 00000000000..c5a5f46605a
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resources/version_sort_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ module Resources
+ class VersionSortEnum < Types::ReleaseSortEnum
+ graphql_name 'CiCatalogResourceVersionSort'
+ description 'Values for sorting catalog resource versions'
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/catalog/resources/version_type.rb b/app/graphql/types/ci/catalog/resources/version_type.rb
new file mode 100644
index 00000000000..8c073690eaf
--- /dev/null
+++ b/app/graphql/types/ci/catalog/resources/version_type.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ module Catalog
+ module Resources
+ # rubocop: disable Graphql/AuthorizeTypes -- Authorization is handled by Ci::Catalog::Resources::VersionsFinder in the resolver.
+ class VersionType < BaseObject
+ graphql_name 'CiCatalogResourceVersion'
+
+ connection_type_class Types::CountableConnectionType
+
+ field :id, ::Types::GlobalIDType[::Ci::Catalog::Resources::Version], null: false,
+ description: 'Global ID of the version.',
+ alpha: { milestone: '16.7' }
+
+ field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the version was created.',
+ alpha: { milestone: '16.7' }
+
+ field :released_at, Types::TimeType, null: true, description: 'Timestamp of when the version was released.',
+ alpha: { milestone: '16.7' }
+
+ field :tag_name, GraphQL::Types::String, null: true, method: :name,
+ description: 'Name of the tag associated with the version.',
+ alpha: { milestone: '16.7' }
+
+ field :tag_path, GraphQL::Types::String, null: true,
+ description: 'Relative web path to the tag associated with the version.',
+ alpha: { milestone: '16.7' }
+
+ field :author, Types::UserType, null: true, description: 'User that created the version.',
+ alpha: { milestone: '16.7' }
+
+ field :commit, Types::CommitType, null: true, complexity: 10, calls_gitaly: true,
+ description: 'Commit associated with the version.',
+ alpha: { milestone: '16.7' }
+
+ def author
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
+ end
+
+ def tag_path
+ Gitlab::Routing.url_helpers.project_tag_path(object.project, object.name)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+ end
+ end
+end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 89f8520b331..564b10ea8e3 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -168,6 +168,18 @@ module Ci
end
# rubocop: enable CodeReuse/ServiceClass
+ def job_artifacts
+ Ci::JobArtifact.none
+ end
+
+ def artifacts_expire_at; end
+
+ def runner; end
+
+ def tag_list
+ ActsAsTaggableOn::TagList.new
+ end
+
def artifacts?
false
end
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index ab4d5c52526..4273c4515bc 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -26,7 +26,7 @@ module Ci
scope :order_by_released_at_asc, -> { joins(:release).keyset_order_by_released_at_asc }
scope :order_by_released_at_desc, -> { joins(:release).keyset_order_by_released_at_desc }
- delegate :name, :description, :tag, :sha, :released_at, :author_id, to: :release
+ delegate :sha, :released_at, :author_id, to: :release
after_destroy :update_catalog_resource
after_save :update_catalog_resource
@@ -114,6 +114,14 @@ module Ci
end
end
+ def name
+ release.tag
+ end
+
+ def commit
+ project.commit_by(oid: sha)
+ end
+
private
def update_catalog_resource
diff --git a/app/services/ml/model_versions/delete_service.rb b/app/services/ml/model_versions/delete_service.rb
new file mode 100644
index 00000000000..4eb8d367a19
--- /dev/null
+++ b/app/services/ml/model_versions/delete_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Ml
+ module ModelVersions
+ class DeleteService
+ def initialize(project, name, version, user)
+ @project = project
+ @name = name
+ @version = version
+ @user = user
+ end
+
+ def execute
+ model_version = Ml::ModelVersion
+ .by_project_id_name_and_version(@project.id, @name, @version)
+ return ServiceResponse.error(message: 'Model not found') unless model_version
+
+ if model_version.package.present?
+ result = ::Packages::MarkPackageForDestructionService
+ .new(container: model_version.package, current_user: @user)
+ .execute
+
+ return ServiceResponse.error(message: result.message) unless result.success?
+ end
+
+ return ServiceResponse.error(message: 'Could not destroy the model version') unless model_version.destroy
+
+ ServiceResponse.success
+ end
+ end
+ end
+end
diff --git a/app/validators/json_schemas/scan_result_policy_project_approval_settings.json b/app/validators/json_schemas/scan_result_policy_project_approval_settings.json
index 5a81c61ede4..4885e5266c1 100644
--- a/app/validators/json_schemas/scan_result_policy_project_approval_settings.json
+++ b/app/validators/json_schemas/scan_result_policy_project_approval_settings.json
@@ -15,7 +15,7 @@
"require_password_to_approve": {
"type": "boolean"
},
- "block_unprotecting_branches": {
+ "block_branch_modification": {
"type": "boolean"
}
}
diff --git a/config/metrics/counts_28d/20210216175055_merge_requests.yml b/config/metrics/counts_28d/20210216175055_merge_requests.yml
index 9e3f64553f6..d46caf93675 100644
--- a/config/metrics/counts_28d/20210216175055_merge_requests.yml
+++ b/config/metrics/counts_28d/20210216175055_merge_requests.yml
@@ -17,6 +17,7 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
removed_by_url:
diff --git a/config/metrics/counts_28d/20210216175542_ci_builds.yml b/config/metrics/counts_28d/20210216175542_ci_builds.yml
index 3d2777aebea..c95a1e9fad5 100644
--- a/config/metrics/counts_28d/20210216175542_ci_builds.yml
+++ b/config/metrics/counts_28d/20210216175542_ci_builds.yml
@@ -16,6 +16,7 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "12.9"
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26441"
diff --git a/config/metrics/counts_28d/20210216181148_service_desk_issues.yml b/config/metrics/counts_28d/20210216181148_service_desk_issues.yml
index 302dc04e16a..9a606ec802d 100644
--- a/config/metrics/counts_28d/20210216181148_service_desk_issues.yml
+++ b/config/metrics/counts_28d/20210216181148_service_desk_issues.yml
@@ -16,5 +16,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_28d/20210216183730_jira.yml b/config/metrics/counts_28d/20210216183730_jira.yml
index 37c9b9a7b66..590ca4e5f37 100644
--- a/config/metrics/counts_28d/20210216183730_jira.yml
+++ b/config/metrics/counts_28d/20210216183730_jira.yml
@@ -16,7 +16,8 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
removed_by_url:
milestone_removed: "<16.4"
diff --git a/config/metrics/counts_28d/20211126091206_p_analytics_ci_cd_lead_time_monthly.yml b/config/metrics/counts_28d/20211126091206_p_analytics_ci_cd_lead_time_monthly.yml
index 63d1ed5d0c6..f5a4882aca5 100644
--- a/config/metrics/counts_28d/20211126091206_p_analytics_ci_cd_lead_time_monthly.yml
+++ b/config/metrics/counts_28d/20211126091206_p_analytics_ci_cd_lead_time_monthly.yml
@@ -12,7 +12,8 @@ time_frame: 28d
data_source: redis_hll
data_category: operational
instrumentation_class: RedisHLLMetric
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
events:
- name: p_analytics_ci_cd_lead_time
unique: user.id
diff --git a/config/metrics/counts_all/20210216175229_auto_devops_enabled.yml b/config/metrics/counts_all/20210216175229_auto_devops_enabled.yml
index 9b192fa26a0..fe90390283f 100644
--- a/config/metrics/counts_all/20210216175229_auto_devops_enabled.yml
+++ b/config/metrics/counts_all/20210216175229_auto_devops_enabled.yml
@@ -17,5 +17,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216175514_ci_external_pipelines.yml b/config/metrics/counts_all/20210216175514_ci_external_pipelines.yml
index b2c70204679..6b830d88d52 100644
--- a/config/metrics/counts_all/20210216175514_ci_external_pipelines.yml
+++ b/config/metrics/counts_all/20210216175514_ci_external_pipelines.yml
@@ -13,5 +13,6 @@ distribution:
- ce
tier:
- free
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
index c9c85bca415..907eed3b822 100644
--- a/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
+++ b/config/metrics/counts_all/20210216180232_projects_jira_dvcs_cloud_active.yml
@@ -19,7 +19,8 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135755
milestone_removed: "<16.6"
diff --git a/config/metrics/counts_all/20210216181011_projects_with_packages.yml b/config/metrics/counts_all/20210216181011_projects_with_packages.yml
index 434d9989ed4..da869a35fa8 100644
--- a/config/metrics/counts_all/20210216181011_projects_with_packages.yml
+++ b/config/metrics/counts_all/20210216181011_projects_with_packages.yml
@@ -16,5 +16,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216181102_issues.yml b/config/metrics/counts_all/20210216181102_issues.yml
index ec679b1089f..f10bbe69e95 100644
--- a/config/metrics/counts_all/20210216181102_issues.yml
+++ b/config/metrics/counts_all/20210216181102_issues.yml
@@ -17,5 +17,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216181254_projects.yml b/config/metrics/counts_all/20210216181254_projects.yml
index 0bd0e783c0a..265f8f394a1 100644
--- a/config/metrics/counts_all/20210216181254_projects.yml
+++ b/config/metrics/counts_all/20210216181254_projects.yml
@@ -16,5 +16,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216182002_remote_mirrors.yml b/config/metrics/counts_all/20210216182002_remote_mirrors.yml
index 0c8103284a3..7f06be1decb 100644
--- a/config/metrics/counts_all/20210216182002_remote_mirrors.yml
+++ b/config/metrics/counts_all/20210216182002_remote_mirrors.yml
@@ -16,5 +16,6 @@ tier:
- free
- premium
- ultimate
-performance_indicator_type: []
+performance_indicator_type:
+- customer_health_score
milestone: "<13.9"
diff --git a/db/docs/audit_events_instance_amazon_s3_configurations.yml b/db/docs/audit_events_instance_amazon_s3_configurations.yml
new file mode 100644
index 00000000000..5cb049342ee
--- /dev/null
+++ b/db/docs/audit_events_instance_amazon_s3_configurations.yml
@@ -0,0 +1,10 @@
+---
+table_name: audit_events_instance_amazon_s3_configurations
+classes:
+ - AuditEvents::Instance::AmazonS3Configuration
+feature_categories:
+ - audit_events
+description: Stores Amazon S3 configurations used for instance level audit event streaming.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136492
+milestone: '16.7'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20231107140642_create_audit_events_instance_amazon_s3_configurations.rb b/db/migrate/20231107140642_create_audit_events_instance_amazon_s3_configurations.rb
new file mode 100644
index 00000000000..8051d23f6a1
--- /dev/null
+++ b/db/migrate/20231107140642_create_audit_events_instance_amazon_s3_configurations.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class CreateAuditEventsInstanceAmazonS3Configurations < Gitlab::Database::Migration[2.2]
+ enable_lock_retries!
+ milestone '16.7'
+
+ UNIQUE_NAME = "unique_instance_amazon_s3_configurations_name"
+ UNIQUE_BUCKET_NAME = "unique_instance_amazon_s3_configurations_bucket_name"
+
+ def change
+ create_table :audit_events_instance_amazon_s3_configurations do |t|
+ t.timestamps_with_timezone null: false
+ t.text :access_key_xid, null: false, limit: 128
+ t.text :name, null: false, limit: 72
+ t.text :bucket_name, null: false, limit: 63
+ t.text :aws_region, null: false, limit: 50
+ t.binary :encrypted_secret_access_key, null: false
+ t.binary :encrypted_secret_access_key_iv, null: false
+
+ t.index [:name], unique: true, name: UNIQUE_NAME
+ t.index [:bucket_name], unique: true, name: UNIQUE_BUCKET_NAME
+ end
+ end
+end
diff --git a/db/schema_migrations/20231107140642 b/db/schema_migrations/20231107140642
new file mode 100644
index 00000000000..e77a46970b3
--- /dev/null
+++ b/db/schema_migrations/20231107140642
@@ -0,0 +1 @@
+7a2cd6460af9afcf6bcbb933854872d2be2b6d098f48383331c83d10c8f9ee73 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 353f009d07c..01eeff3dfa6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12713,6 +12713,31 @@ CREATE SEQUENCE audit_events_id_seq
ALTER SEQUENCE audit_events_id_seq OWNED BY audit_events.id;
+CREATE TABLE audit_events_instance_amazon_s3_configurations (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ access_key_xid text NOT NULL,
+ name text NOT NULL,
+ bucket_name text NOT NULL,
+ aws_region text NOT NULL,
+ encrypted_secret_access_key bytea NOT NULL,
+ encrypted_secret_access_key_iv bytea NOT NULL,
+ CONSTRAINT check_1a908bd36f CHECK ((char_length(name) <= 72)),
+ CONSTRAINT check_8083750c42 CHECK ((char_length(bucket_name) <= 63)),
+ CONSTRAINT check_d2ca3eb90e CHECK ((char_length(aws_region) <= 50)),
+ CONSTRAINT check_d6d6bd8e8b CHECK ((char_length(access_key_xid) <= 128))
+);
+
+CREATE SEQUENCE audit_events_instance_amazon_s3_configurations_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE audit_events_instance_amazon_s3_configurations_id_seq OWNED BY audit_events_instance_amazon_s3_configurations.id;
+
CREATE TABLE audit_events_instance_external_audit_event_destinations (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -26171,6 +26196,8 @@ ALTER TABLE ONLY audit_events_external_audit_event_destinations ALTER COLUMN id
ALTER TABLE ONLY audit_events_google_cloud_logging_configurations ALTER COLUMN id SET DEFAULT nextval('audit_events_google_cloud_logging_configurations_id_seq'::regclass);
+ALTER TABLE ONLY audit_events_instance_amazon_s3_configurations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_amazon_s3_configurations_id_seq'::regclass);
+
ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_external_audit_event_destinations_id_seq'::regclass);
ALTER TABLE ONLY audit_events_instance_google_cloud_logging_configurations ALTER COLUMN id SET DEFAULT nextval('audit_events_instance_google_cloud_logging_configuration_id_seq'::regclass);
@@ -28014,6 +28041,9 @@ ALTER TABLE ONLY audit_events_external_audit_event_destinations
ALTER TABLE ONLY audit_events_google_cloud_logging_configurations
ADD CONSTRAINT audit_events_google_cloud_logging_configurations_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY audit_events_instance_amazon_s3_configurations
+ ADD CONSTRAINT audit_events_instance_amazon_s3_configurations_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations
ADD CONSTRAINT audit_events_instance_external_audit_event_destinations_pkey PRIMARY KEY (id);
@@ -35261,6 +35291,10 @@ CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_eve
CREATE UNIQUE INDEX unique_index_sysaccess_ms_access_tokens_on_sysaccess_ms_app_id ON system_access_microsoft_graph_access_tokens USING btree (system_access_microsoft_application_id);
+CREATE UNIQUE INDEX unique_instance_amazon_s3_configurations_bucket_name ON audit_events_instance_amazon_s3_configurations USING btree (bucket_name);
+
+CREATE UNIQUE INDEX unique_instance_amazon_s3_configurations_name ON audit_events_instance_amazon_s3_configurations USING btree (name);
+
CREATE UNIQUE INDEX unique_instance_audit_event_destination_name ON audit_events_instance_external_audit_event_destinations USING btree (name);
CREATE UNIQUE INDEX unique_instance_google_cloud_logging_configurations ON audit_events_instance_google_cloud_logging_configurations USING btree (google_project_id_name, log_id_name);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a1fb13e846c..d2119e4434c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -8925,6 +8925,30 @@ The edge type for [`CiCatalogResource`](#cicatalogresource).
| <a id="cicatalogresourceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cicatalogresourceedgenode"></a>`node` | [`CiCatalogResource`](#cicatalogresource) | The item at the end of the edge. |
+#### `CiCatalogResourceVersionConnection`
+
+The connection type for [`CiCatalogResourceVersion`](#cicatalogresourceversion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cicatalogresourceversionconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
+| <a id="cicatalogresourceversionconnectionedges"></a>`edges` | [`[CiCatalogResourceVersionEdge]`](#cicatalogresourceversionedge) | A list of edges. |
+| <a id="cicatalogresourceversionconnectionnodes"></a>`nodes` | [`[CiCatalogResourceVersion]`](#cicatalogresourceversion) | A list of nodes. |
+| <a id="cicatalogresourceversionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiCatalogResourceVersionEdge`
+
+The edge type for [`CiCatalogResourceVersion`](#cicatalogresourceversion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cicatalogresourceversionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cicatalogresourceversionedgenode"></a>`node` | [`CiCatalogResourceVersion`](#cicatalogresourceversion) | The item at the end of the edge. |
+
#### `CiConfigGroupConnection`
The connection type for [`CiConfigGroup`](#ciconfiggroup).
@@ -15002,7 +15026,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cicatalogresourceicon"></a>`icon` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Icon for the catalog resource. |
| <a id="cicatalogresourceid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. ID of the catalog resource. |
| <a id="cicatalogresourcelatestreleasedat"></a>`latestReleasedAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Release date of the catalog resource's latest version. |
-| <a id="cicatalogresourcelatestversion"></a>`latestVersion` **{warning-solid}** | [`Release`](#release) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Latest version of the catalog resource. |
+| <a id="cicatalogresourcelatestversion"></a>`latestVersion` **{warning-solid}** | [`CiCatalogResourceVersion`](#cicatalogresourceversion) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Latest version of the catalog resource. |
| <a id="cicatalogresourcename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Name of the catalog resource. |
| <a id="cicatalogresourceopenissuescount"></a>`openIssuesCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.3. This feature is an Experiment. It can be changed or removed at any time. Count of open issues that belong to the the catalog resource. |
| <a id="cicatalogresourceopenmergerequestscount"></a>`openMergeRequestsCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.3. This feature is an Experiment. It can be changed or removed at any time. Count of open merge requests that belong to the the catalog resource. |
@@ -15021,7 +15045,7 @@ WARNING:
**Introduced** in 16.2.
This feature is an Experiment. It can be changed or removed at any time.
-Returns [`ReleaseConnection`](#releaseconnection).
+Returns [`CiCatalogResourceVersionConnection`](#cicatalogresourceversionconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
@@ -15031,7 +15055,21 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="cicatalogresourceversionssort"></a>`sort` | [`ReleaseSort`](#releasesort) | Sort releases by given criteria. |
+| <a id="cicatalogresourceversionssort"></a>`sort` | [`CiCatalogResourceVersionSort`](#cicatalogresourceversionsort) | Sort versions by given criteria. |
+
+### `CiCatalogResourceVersion`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cicatalogresourceversionauthor"></a>`author` **{warning-solid}** | [`UserCore`](#usercore) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. User that created the version. |
+| <a id="cicatalogresourceversioncommit"></a>`commit` **{warning-solid}** | [`Commit`](#commit) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Commit associated with the version. |
+| <a id="cicatalogresourceversioncreatedat"></a>`createdAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Timestamp of when the version was created. |
+| <a id="cicatalogresourceversionid"></a>`id` **{warning-solid}** | [`CiCatalogResourcesVersionID!`](#cicatalogresourcesversionid) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Global ID of the version. |
+| <a id="cicatalogresourceversionreleasedat"></a>`releasedAt` **{warning-solid}** | [`Time`](#time) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Timestamp of when the version was released. |
+| <a id="cicatalogresourceversiontagname"></a>`tagName` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Name of the tag associated with the version. |
+| <a id="cicatalogresourceversiontagpath"></a>`tagPath` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Relative web path to the tag associated with the version. |
### `CiConfig`
@@ -28391,6 +28429,17 @@ Values for sorting catalog resources.
| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. |
+### `CiCatalogResourceVersionSort`
+
+Values for sorting catalog resource versions.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="cicatalogresourceversionsortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
+| <a id="cicatalogresourceversionsortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
+| <a id="cicatalogresourceversionsortreleased_at_asc"></a>`RELEASED_AT_ASC` | Released at by ascending order. |
+| <a id="cicatalogresourceversionsortreleased_at_desc"></a>`RELEASED_AT_DESC` | Released at by descending order. |
+
### `CiConfigIncludeType`
Include type.
@@ -31007,6 +31056,12 @@ A `CiCatalogResourceID` is a global ID. It is encoded as a string.
An example `CiCatalogResourceID` is: `"gid://gitlab/Ci::Catalog::Resource/1"`.
+### `CiCatalogResourcesVersionID`
+
+A `CiCatalogResourcesVersionID` is a global ID. It is encoded as a string.
+
+An example `CiCatalogResourcesVersionID` is: `"gid://gitlab/Ci::Catalog::Resources::Version/1"`.
+
### `CiJobArtifactID`
A `CiJobArtifactID` is a global ID. It is encoded as a string.
diff --git a/doc/architecture/blueprints/gitlab_steps/data.drawio.png b/doc/architecture/blueprints/gitlab_steps/data.drawio.png
new file mode 100644
index 00000000000..59436093fb7
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/data.drawio.png
Binary files differ
diff --git a/doc/architecture/blueprints/gitlab_steps/implementation.md b/doc/architecture/blueprints/gitlab_steps/implementation.md
new file mode 100644
index 00000000000..add6097b08f
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/implementation.md
@@ -0,0 +1,350 @@
+---
+owning-stage: "~devops::verify"
+description: Implementation details for [CI Steps](index.md).
+---
+
+# Design and implementation details
+
+## Baseline Step Proto
+
+The internals of Step Runner operate on the baseline step definition
+which is defined in Protocol Buffer. All GitLab CI steps (and other
+supported formats such as GitHub Actions) compile / fold to baseline steps.
+Both step invocations in `.gitlab-ci.yml` and step definitions
+in `step.yml` files will be compiled to baseline structures.
+The term "step" means "baseline step" for the remainder of this document.
+
+Each step includes a reference `ref` in the form of a URI. The method of
+retrieval is determined by the protocol of the URI.
+
+Steps and step traces have fields for inputs, outputs,
+environment variables and environment exports.
+After steps are downloaded and the `step.yml` is parsed
+a step definition `def` will be added.
+If a step defines multiple additional steps then the
+trace will include sub-traces for each sub-step.
+
+```protobuf
+message Step {
+ string name = 1;
+ Reference ref = 2;
+ repeated EnvironmentVariable env = 3;
+ repeated Input inputs = 4;
+ message Reference {
+ string uri = 1;
+ string version = 2;
+ string hash = 3;
+ Definition def = 4;
+ }
+}
+message Definition {
+ Spec spec = 1;
+ enum Type {
+ type_unknown = 0;
+ type_steps = 1;
+ type_exec = 2;
+ }
+ Type type = 2;
+ oneof type_oneof {
+ DefinitionExec exec = 3;
+ DefinitionSteps steps = 4;
+ }
+ message Exec {
+ repeated string command = 1;
+ string work_dir = 2;
+ }
+ message Steps {
+ repeated Step children = 1;
+ }
+}
+
+message Spec {
+ repeated Input inputs = 1;
+ message Input {
+ string key = 1;
+ string default_value = 2;
+ }
+}
+
+message EnvironmentVariable {
+ string key = 1;
+ string value = 2;
+ bool masked = 3;
+ bool raw = 4;
+}
+
+message Input {
+ string key = 1;
+ string value = 2;
+ bool masked = 3;
+ bool raw = 4;
+}
+
+message Output {
+ string key = 1;
+ string value = 2;
+ bool masked = 3;
+}
+
+message StepResult {
+ Step step = 1;
+ enum Status {
+ unknown_result = 0;
+ success = 1;
+ failure = 2;
+ running = 3;
+ }
+ Result result = 2;
+ repeated Output outputs = 3;
+ repeated EnvironmentVariable exports = 4;
+ int32 exit_code = 5;
+ repeated StepResult children_step_results = 6;
+}
+```
+
+## Step Caching
+
+Steps are cached locally by a key comprised of `location`
+(URL), `version` and `hash`. This prevents the exact same component
+from being downloaded multiple times. The first time a step is
+referenced it will be downloaded (unless local) and the cache will
+return the path to the folder containing `step.yml` and the other
+step files. If the same step is referenced again, the same folder
+will be returned without downloading.
+
+If a step is referenced which differs by version or hash from another
+cached step, it will be re-downloaded into a different folder and
+cached separately.
+
+## Execution Context
+
+State is kept by Step Runner across all steps in the form of
+an execution context. The context contains the output of each step,
+environment variables and overall job and environment metadata.
+The execution context can be referenced by expressions in
+GitLab CI steps provided by the workflow author.
+
+Example of context available to expressions in `.gitlab-ci.yml`:
+
+```yaml
+steps:
+ previous_step:
+ outputs:
+ name: "hello world"
+env:
+ EXAMPLE_VAR: "bar"
+job:
+ id: 1234
+```
+
+Expressions in step definitions can also reference execution
+context. However they can only access overall
+job and environment metadata and the inputs defined in `step.yml`.
+They cannot access the outputs of previous steps. In order to
+provide the output of one step to the next, the step input
+values should include an expression which references another
+step's output.
+
+Example of context available to expressions in `step.yml`:
+
+```yaml
+inputs:
+ name: "foo"
+env:
+ EXAMPLE_VAR: "bar"
+job:
+ id: 1234
+```
+
+E.g. this is not allowed in a `step.yml file` because steps
+should not couple to one another.
+
+```yaml
+spec:
+ inputs:
+ name:
+---
+type: exec
+exec:
+ command: [echo, hello, ${{ steps.previous_step.outputs.name }}]
+```
+
+This is allowed because the GitLab CI steps syntax passes data
+from one step to another:
+
+```yaml
+spec:
+ inputs:
+ name:
+---
+type: exec
+exec:
+ command: [echo, hello, ${{ inputs.name }}]
+```
+
+```yaml
+steps:
+- name: previous_step
+ ...
+- name: greeting
+ inputs:
+ name: ${{ steps.previous_step.outputs.name }}
+```
+
+Therefore evaluation of expressions will done in two different kinds
+of context. One as a GitLab CI Step and one as a step definition.
+
+### Step Inputs
+
+Step inputs can be given in several ways. They can be embeded
+directly into expressions in an `exec` command (as above). Or they
+can be embedded in expressions for environment variables set during
+exec:
+
+```yaml
+spec:
+ inputs:
+ name:
+---
+type: exec
+exec:
+ command: [greeting.sh]
+env:
+ NAME: ${{ inputs.name }}
+```
+
+### Input Types
+
+Input values are stored as strings. But they can also have a type
+associated with them. Supported types are:
+
+- `string`
+- `bool`
+- `number`
+- `object`
+
+String type values can be any string. Bool type values must be either `true`
+or `false` when parsed as JSON. Number type values must a valid float64
+when parsed as JSON. Object types will be a JSON serialization of
+the YAML input structure.
+
+For example, these would be valid inputs:
+
+```yaml
+steps:
+- name: my_step
+ inputs:
+ foo: bar
+ baz: true
+ bam: 1
+```
+
+Given this step definition:
+
+```yaml
+spec:
+ inputs:
+ foo:
+ type: string
+ baz:
+ type: bool
+ bam:
+ type: number
+---
+type: exec
+exec:
+ command: [echo, ${{ inputs.foo }}, ${{ inputs.baz }}, ${{ inputs.bam }}]
+```
+
+And it would output `bar true 1`
+
+For an object type, these would be valid inputs:
+
+```yaml
+steps:
+ name: my_step
+ inputs:
+ foo:
+ steps:
+ - name: my_inner_step
+ inputs:
+ name: steppy
+```
+
+Given this step definition:
+
+```yaml
+spec:
+ inputs:
+ foo:
+ type: object
+---
+type: exec
+exec:
+ command: [echo, ${{ inputs.foo }}]
+```
+
+And it would output `{"steps":[{"name":"my_inner_step","inputs":{"name":"steppy"}}]}`
+
+### Outputs
+
+Output files are created into which steps can write their
+outputs and environment variable exports. The file locations are
+provided in `OUTPUT_FILE` and `ENV_FILE` environment variables.
+
+After execution Step Runner will read the output and environment
+variable files and populate the trace with their values. The
+outputs will be stored under the context for the executed step.
+And the exported environment variables will be merged with environment
+provided to the next step.
+
+Some steps can be of type `steps` and be composed of a sequence
+of GitLab CI steps. These will be compiled and executed in sequence.
+Any environment variables exported by nested steps will be available
+to subsequent steps. And will be available to high level steps
+when the nested steps are complete. E.g. entering nested steps does
+not create a new "scope" or context object. Environment variables
+are global.
+
+## Containers
+
+We've tried a couple approaches to running steps in containers.
+In end we've decided to delegate steps entirely to a step runner
+in the container.
+
+Here are the options considered:
+
+### Delegation (chosen option)
+
+A provision is made for passing complex structures to steps, which
+is to serialize them as JSON (see Inputs above). In this way the actual
+step to be run can be merely a parameter to step running in container.
+So the outer step is a `docker/run` step with a command that executes
+`step-runner` with a `steps` input parameter. The `docker/run` step will
+run the container and then extract the output files from the container
+and re-emit them to the outer steps.
+
+This same technique will work for running steps in VMs or whatever.
+Step Runner doesn't have to know anything about containerizing or
+isolation steps.
+
+### Special Compilation (rejected option)
+
+When we see the `image` keyword in a GitLab CI step we would download
+and compile the "target" step. Then manufacture a `docker/run` step
+and pass the complied `exec` command as an input. Then we would compile
+the `docker/run` step and execute it.
+
+However this requires Step Runner to know how to construct a `docker/run`
+step. Which couples Step Runner with the method of isolation, making
+isolation in VMs and other methods more complicated.
+
+### Native Docker (rejected option)
+
+The baseline step can include provisions for running a step in a
+Docker container. For example the step could include a `ref` "target"
+field and an `image` field.
+
+However this also couples Step Runner with Docker and expands the role
+of Step Runner. It is preferable to make Docker an external step
+that Step Runner execs in the same way as any other step.
diff --git a/doc/architecture/blueprints/gitlab_steps/index.md b/doc/architecture/blueprints/gitlab_steps/index.md
index 5e3becfec19..fb8b17e26a1 100644
--- a/doc/architecture/blueprints/gitlab_steps/index.md
+++ b/doc/architecture/blueprints/gitlab_steps/index.md
@@ -1,7 +1,7 @@
---
status: proposed
creation-date: "2023-08-23"
-authors: [ "@ayufan" ]
+authors: [ "@ayufan", "@josephburnett" ]
coach: "@grzegorz"
approvers: [ "@dhershkovitch", "@DarrenEastman", "@cheryl.li" ]
owning-stage: "~devops::verify"
@@ -57,7 +57,7 @@ have to be part of CI syntax. Instead, they can be provided in the form of reusa
that are configured in a generic way in the CI config, and later downloaded and executed according
to inputs and parameters.
-The GitLab Steps is meant to fill that product-gap by following similar model to competitors
+GitLab Steps is meant to fill that product-gap by following similar model to competitors
and to some extent staying compatible with them. The GitLab Steps is meant to replace all
purpose-specific syntax to handle specific features. By providing and using reusable components,
that are build outside of `.gitlab-ci.yml`, that are versioned, and requested when needed
@@ -131,6 +131,80 @@ TBD
## Proposal
+Step Runner will be a new go binary which lives at `https://gitlab.com/gitlab-org/step-runner`.
+It will be able to accept a number of input formats which are compiled to a standard proto format.
+Output will be a standard proto trace which will include details for debugging and reproducing the build.
+
+### Capabilities
+
+- Read steps
+ - from environment variable
+ - from `.gitlab-ci.yml` file
+ - from gRPC server in step-runner
+ - from commandline JSON input
+- Compile GitLab Steps and GitHub Actions to a baseline step definition
+ - explicit inputs and outputs
+ - explicit environment and exports
+ - baseline steps can be type `exec` or more steps
+- Download and run steps from:
+ - Git repos
+ - zip files
+ - locally provided
+- A job can be composed of different kinds of steps
+ - steps can come from different sources and be run in different ways
+ - steps can access environment exports and output of previous steps
+- Produce a step-by-step trace of execution
+ - including final inputs and outputs
+ - including final environment and exports
+ - including logs of each step
+ - each step specifies the exact runtime and component used (hash)
+ - (optional) masking sensitive inputs, outputs, environment and exports
+- Replaying a trace
+ - reuses the exact runtimes and components from trace
+ - output of trace will be the same trace if build is deterministic
+
+### Example invocations
+
+#### Commandline
+
+- `STEPS=$(cat steps.yml) step-runner ci`
+- `step-runner local .gitlab-ci.yml --format gitlab-ci --job-name hello-world --output-file trace.json`
+- `step-runner replay trace.json`
+- `step-runner ci --port 8080`
+
+#### GitLab CI
+
+```yaml
+hello-world:
+ image: registry.gitlab.com/gitlab-org/step-runner
+ variables:
+ STEPS: |
+ - step: gitlab.com/josephburnett/component-hello-steppy@master
+ inputs:
+ greeting: "hello ${{ env.name }}"
+ env:
+ name: world
+ script:
+ - /step-runner ci
+ artifacts:
+ paths:
+ - trace.json
+```
+
+### Basic compilation and execution process
+
+Steps as expressed in GitLab CI are complied to the baseline step definition.
+Referenced steps are loaded and compliled to produce an `exec` command,
+or to produce an additional list of GitLab CI steps which are compiled recursively.
+Each steps is executed immediately after compilation so its output will be available for subsequent compilations.
+
+![diagram of data during compilation](data.drawio.png)
+
+Steps return outputs and exports via files which are collected by Step Runner after each step.
+Finally all the compiled inputs and outputs for each step are collected in a step trace.
+
+![sequenced diagram of step runner compilation and execution](step-runner-sequence.drawio.png)
+
### GitLab Steps definition and syntax
- [Step Definition](step-definition.md).
@@ -142,8 +216,20 @@ TBD
## Design and implementation details
-TBD
+See [implementation.md](implementation.md) and [runner-integration.md](runner-integration.md).
## References
+- [GitLab Issue #215511](https://gitlab.com/gitlab-org/gitlab/-/issues/215511)
+- [Step Runner Code](https://gitlab.com/josephburnett/step-runner/-/tree/blueprint2).
+ This is the exploratory code created during the writing of this blueprint.
+ It shows the structure of the Step Runner binary and how the pieces fit together.
+ It runs but doesn't quite do the right thing (see all the TODOs).
+- [CI Steps / CI Events / Executors / Taskonaut (video)](https://youtu.be/nZoO547IISM).
+ Some high-level discussion about how these 4 blueprints relate to each other.
+ And a good prequel to the video about this MR.
+- [Steps in Runner (video)](https://youtu.be/82WLQ4zHYts).
+ A walk through of the Step Runner details from the code perspective.
+- [CI YAML keywords](https://gitlab.com/gitlab-org/gitlab/-/issues/398129#note_1324467337).
+ An inventory of affected keywords.
- [GitLab Epic 11535](https://gitlab.com/groups/gitlab-org/-/epics/11535)
diff --git a/doc/architecture/blueprints/gitlab_steps/runner-integration.md b/doc/architecture/blueprints/gitlab_steps/runner-integration.md
new file mode 100644
index 00000000000..e5a635908bc
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/runner-integration.md
@@ -0,0 +1,116 @@
+---
+owning-stage: "~devops::verify"
+description: Runner integration for [CI Steps](index.md).
+---
+
+# Runner Integration
+
+Steps are delivered to Step Runner as a YAML blob in the GitLab CI syntax.
+Runner interacts with Step Runner over a gRPC service `StepRunner`
+which is started on a local socket in the execution environment. This
+is the same way that Nesting serves a gRPC service in a dedicated
+Mac instance. The service has three RPCs, `run`, `follow` and `cancel`.
+
+Run is the initial delivery of the steps. Follow requests a streaming
+response to step traces. And Cancel stops execution and cleans up
+resources as soon as possible.
+
+Step Runner operating in gRPC mode will be able to executed multiple
+step payloads at once. That is each call to `run` will start a new
+goroutine and execute the steps until completion. Multiple calls to `run`
+may be made simultaneously. This is also why components are cached by
+`location`, `version` and `hash`. Because we cannot be changing which
+ref we are on while multiple, concurrent executions are using the
+underlying files.
+
+```proto
+service StepRunner {
+ rpc Run(RunRequest) returns (RunResponse);
+ rpc Follow(FollowRequest) returns (stream FollowResponse);
+ rpc Cancel(CancelRequest) returns (CancelResponse);
+}
+
+message RunRequest {
+ string id = 1;
+ oneof job_oneof {
+ string ci_job = 2;
+ Steps steps = 3;
+ }
+}
+
+message RunResponse {
+}
+
+message FollowRequest {
+ string id = 1;
+}
+
+message FollowResponse {
+ StepResult result = 1;
+}
+
+message CancelRequest {
+ string id = 1;
+}
+
+message CancelResponse {
+}
+```
+
+As steps are executed, traces are streamed back to GitLab Runner.
+So execution can be followed at least at the step level. If a more
+granular follow is required, we can introduce a gRPC step type which
+can stream back logs as they are produced.
+
+Here is how we will connect to Step Runner in each runner executor:
+
+## Instance
+
+The Instance executor is accessed via SSH, the same as today. However
+instead of starting a bash shell and piping in commands, it connects
+to the Step Runner socket in a known location and makes gRPC
+calls. This is the same as how Runner calls the Nesting server in
+dedicated Mac instances to make VMs.
+
+This requires that Step Runner is present and started in the job
+execution environment.
+
+## Docker
+
+The same requirement that Step Runner is present and started is true
+for the Docker executor (and `docker-autoscaler`). However in order to
+connect to the socket inside the container, we must `exec` a bridge
+process in the container. This will be another command on the Step
+Runner binary which proxies STDIN and STDOUT to the local socket in a
+known location, allowing the caller of exec to make gRPC calls inside
+the container.
+
+## Kubernetes
+
+The Kubelet on Kubernetes Nodes exposes an exec API which will start a
+process in a container of a running Pod. We will use this exec create
+a bridge process that will allow the caller to make gRPC calls inside
+the Pod. Same as the Docker executor.
+
+In order to access to this protected Kubelet API we must use the
+Kubernetes API which provides an exec sub-resource on Pod. A caller
+can POST to the URL of a pod suffixed with `/exec` and then negotiate
+the connection up to a SPDY protocol for bidirectional byte
+streaming. So GitLab Runner can use the Kubernetes API to connect to
+the Step Runner service and deliver job payloads.
+
+This is the same way that `kubectl exec` works. In fact most of the
+internals such as SPDY negotiation are provided as `client-go`
+libraries. So Runner can call the Kubernetes API directly by
+importing the necessary libraries rather than shelling out to
+Kubectl.
+
+Historically one of the weaknesses of the Kubernetes Executor was
+running a whole job through a single exec. To mitigate this Runner
+uses the attach command instead, which can "re-attach" to an existing
+shell process and pick up where it left off.
+
+This is not necessary for Step Runner however, because the exec is
+just establishing a bridge to the long-running gRPC process. If the
+connection drops, Runner will just "re-attach" by exec'ing another
+connection and continuing to make RPC calls like `follow`.
diff --git a/doc/architecture/blueprints/gitlab_steps/step-runner-sequence.drawio.png b/doc/architecture/blueprints/gitlab_steps/step-runner-sequence.drawio.png
new file mode 100644
index 00000000000..9f6a6dcad9f
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_steps/step-runner-sequence.drawio.png
Binary files differ
diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md
index 91502fb202e..0e9a6ea1d72 100644
--- a/doc/user/application_security/policies/scan-result-policies.md
+++ b/doc/user/application_security/policies/scan-result-policies.md
@@ -181,11 +181,12 @@ the defined policy.
> - The `block_unprotecting_branches` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/423101) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_policy_settings`. Disabled by default.
> - The `scan_result_policy_settings` feature flag was replaced by the `scan_result_policies_block_unprotecting_branches` feature flag in 16.4.
+> - The `block_unprotecting_branches` field was [replaced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137153) by `block_branch_modification` field in GitLab 16.7.
> - The `prevent_approval_by_author`, `prevent_approval_by_commit_author`, `remove_approvals_with_new_commit`, and `require_password_to_approve` fields were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418752) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_any_merge_request`. Enabled by default.
> - The `prevent_pushing_and_force_pushing` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420629) in GitLab 16.4 [with flag](../../../administration/feature_flags.md) named `scan_result_policies_block_force_push`. Enabled by default.
FLAG:
-On self-managed GitLab, by default the `block_unprotecting_branches` field is unavailable. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`. On GitLab.com, this feature is unavailable.
+On self-managed GitLab, by default the `block_branch_modification` field is unavailable. To show the feature, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_unprotecting_branches`. On GitLab.com, this feature is unavailable.
On self-managed GitLab, by default the `prevent_approval_by_author`, `prevent_approval_by_commit_author`, `remove_approvals_with_new_commit`, and `require_password_to_approve` fields are available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_result_any_merge_request`. On GitLab.com, this feature is available.
On self-managed GitLab, by default the `prevent_pushing_and_force_pushing` field is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_result_policies_block_force_push`. On GitLab.com, this feature is available.
@@ -193,7 +194,7 @@ The settings set in the policy overwrite settings in the project.
| Field | Type | Required | Possible values | Applicable rule type | Description |
|-------------------------------------|-----------|----------|-----------------|----------------------|-------------|
-| `block_unprotecting_branches` | `boolean` | false | `true`, `false` | All | When enabled, prevents a user from removing a branch from the protected branches list, deleting a protected branch, or changing the default branch if that branch is included in the security policy. This ensures users cannot remove protection status from a branch to merge vulnerable code. |
+| `block_branch_modification` | `boolean` | false | `true`, `false` | All | When enabled, prevents a user from removing a branch from the protected branches list, deleting a protected branch, or changing the default branch if that branch is included in the security policy. This ensures users cannot remove protection status from a branch to merge vulnerable code. |
| `prevent_approval_by_author` | `boolean` | false | `true`, `false` | `Any merge request` | When enabled, merge request authors cannot approve their own MRs. This ensures code authors cannot introduce vulnerabilities and approve code to merge. |
| `prevent_approval_by_commit_author` | `boolean` | false | `true`, `false` | `Any merge request` | When enabled, users who have contributed code to the MR are ineligible for approval. This ensures code committers cannot introduce vulnerabilities and approve code to merge. |
| `remove_approvals_with_new_commit` | `boolean` | false | `true`, `false` | `Any merge request` | When enabled, if an MR receives all necessary approvals to merge, but then a new commit is added, new approvals are required. This ensures new commits that may include vulnerabilities cannot be introduced. |
diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md
index 2ccb42b93eb..d625f132ada 100644
--- a/doc/user/group/manage.md
+++ b/doc/user/group/manage.md
@@ -83,6 +83,11 @@ It is not possible to rename a namespace if it contains a
project with [Container Registry](../packages/container_registry/index.md) tags,
because the project cannot be moved.
+WARNING:
+To ensure that groups with thousands of subgroups get processed correctly, you should test the path change in a test environment.
+Consider increasing the [Puma worker timeout](../../administration/operations/puma.md#change-the-worker-timeout) temporarily.
+For more information about our solution to mitigate this timeout risk, see [issue 432065](https://gitlab.com/gitlab-org/gitlab/-/issues/432065).
+
## Change the default branch protection of a group
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7583) in GitLab 12.9.
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index d4c4f94c7d3..d1153a0990e 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -34,7 +34,7 @@ module Gitlab
pipeline
.stages
.flat_map(&:statuses)
- .select { |status| status.respond_to?(:tag_list) }
+ .select { |status| status.respond_to?(:tag_list=) }
end
end
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index d3dc955d9e2..401ac50509d 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -109,6 +109,8 @@ module Gitlab
end
def secret_file
+ return unless defined?(Settings)
+
if raw_config_hash[:secret_file].blank?
File.join(Settings.encrypted_settings['path'], 'redis.yaml.enc')
else
@@ -130,9 +132,11 @@ module Gitlab
# the file. In other cases, like when being loaded as part of spinning
# up test environment via `scripts/setup-test-env`, we should gate on
# the presence of the specified secret file so that
- # `Settings.encrypted`, which might not be loadable does not gets
- # called.
- Settings.encrypted(secret_file) if File.exist?(secret_file) || ::Gitlab::Runtime.rake?
+ # `Settings.encrypted`, which might not be loadable does not get
+ # called. Same is the case when this library gets called by Mailroom
+ # which does not have rails environment available.
+ Settings.encrypted(secret_file) if (secret_file && File.exist?(secret_file)) ||
+ (defined?(Gitlab::Runtime) && Gitlab::Runtime.rake?)
end
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4e2e4108c22..8f8c640e232 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27875,7 +27875,7 @@ msgstr ""
msgid "Last Accessed On"
msgstr ""
-msgid "Last Activity"
+msgid "Last GitLab activity"
msgstr ""
msgid "Last Name"
@@ -42328,7 +42328,7 @@ msgstr ""
msgid "ScanResultPolicy|Prevent approval by merge request's author"
msgstr ""
-msgid "ScanResultPolicy|Prevent branch protection modification"
+msgid "ScanResultPolicy|Prevent branch modification"
msgstr ""
msgid "ScanResultPolicy|Prevent pushing and force pushing"
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 7d80ab7b15d..db56b754623 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -69,5 +69,9 @@ FactoryBot.define do
deployment.succeed!
end
end
+
+ trait :with_bridge do
+ deployable { association :ci_bridge, environment: environment.name, pipeline: association(:ci_pipeline, project: environment.project) }
+ end
end
end
diff --git a/spec/finders/ci/catalog/resources/versions_finder_spec.rb b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
index b2418aa45dd..b541b84f198 100644
--- a/spec/finders/ci/catalog/resources/versions_finder_spec.rb
+++ b/spec/finders/ci/catalog/resources/versions_finder_spec.rb
@@ -22,13 +22,13 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli
end.not_to exceed_query_limit(control_count)
end
- context 'when the user is not authorized for any catalog resource' do
+ context 'when the user is not authorized' do
it 'returns empty response' do
is_expected.to be_empty
end
end
- describe 'versions' do
+ context 'when the user is authorized' do
before_all do
resource1.project.add_guest(current_user)
end
@@ -74,7 +74,7 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli
end
end
- describe 'latest versions' do
+ context 'when `latest` parameter is true' do
before_all do
resource1.project.add_guest(current_user)
resource2.project.add_guest(current_user)
@@ -85,22 +85,5 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli
it 'returns the latest version for each authorized catalog resource' do
expect(execute).to match_array([v1_1, v2_1])
end
-
- context 'when one catalog resource does not have versions' do
- it 'returns the latest version of only the catalog resource with versions' do
- resource1.versions.delete_all(:delete_all)
-
- is_expected.to match_array([v2_1])
- end
- end
-
- context 'when no catalog resource has versions' do
- it 'returns empty response' do
- resource1.versions.delete_all(:delete_all)
- resource2.versions.delete_all(:delete_all)
-
- is_expected.to be_empty
- end
- end
end
end
diff --git a/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js
index f7d82d2b662..953e6173662 100644
--- a/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js
@@ -1,5 +1,5 @@
import { reportIssues, parsedReportIssues } from 'jest/ci/reports/codequality_report/mock_data';
-import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/utils/codequality_parser';
describe('Codequality report store utils', () => {
let result;
diff --git a/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js
new file mode 100644
index 00000000000..5f530f2c3be
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+
+import ImportHistoryLink from '~/import_entities/import_groups/components/import_history_link.vue';
+
+describe('import history link', () => {
+ let wrapper;
+
+ const mockHistoryPath = '/import/history';
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMount(ImportHistoryLink, {
+ propsData: {
+ historyPath: mockHistoryPath,
+ ...props,
+ },
+ });
+ };
+
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ it('renders link with href', () => {
+ const mockId = 174;
+
+ createComponent({
+ props: {
+ id: mockId,
+ },
+ });
+
+ expect(findGlLink().text()).toBe('View details');
+ expect(findGlLink().attributes('href')).toBe('/import/history?bulk_import_id=174');
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 8cfc9d3ccd7..4141bded502 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -13,6 +13,7 @@ import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
+import ImportHistoryLink from '~/import_entities/import_groups/components//import_history_link.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
@@ -179,6 +180,23 @@ describe('import table', () => {
expect(findAllImportStatuses().wrappers.map((w) => w.text())).toEqual(expectedStatuses);
});
+ it('renders import history link for imports with id', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: FAKE_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ const importHistoryLinks = wrapper.findAllComponents(ImportHistoryLink);
+
+ expect(importHistoryLinks).toHaveLength(2);
+ expect(importHistoryLinks.at(0).props('id')).toBe(FAKE_GROUPS[1].id);
+ expect(importHistoryLinks.at(1).props('id')).toBe(FAKE_GROUPS[3].id);
+ });
+
it('correctly maintains root namespace as last import target', async () => {
createComponent({
bulkImportSourceGroups: () => ({
diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js
index e7ed712c309..30380bcf6a5 100644
--- a/spec/frontend/organizations/users/components/app_spec.js
+++ b/spec/frontend/organizations/users/components/app_spec.js
@@ -4,10 +4,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
+import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql';
import OrganizationsUsersApp from '~/organizations/users/components/app.vue';
import OrganizationsUsersView from '~/organizations/users/components/users_view.vue';
-import { MOCK_ORGANIZATION_GID, MOCK_USERS, MOCK_USERS_FORMATTED } from '../mock_data';
+import {
+ MOCK_ORGANIZATION_GID,
+ MOCK_USERS,
+ MOCK_USERS_FORMATTED,
+ MOCK_PAGE_INFO,
+} from '../mock_data';
jest.mock('~/alert');
@@ -16,10 +22,11 @@ Vue.use(VueApollo);
const mockError = new Error();
const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
-const successfulResolver = (nodes) =>
- jest.fn().mockResolvedValue({
- data: { organization: { id: 1, organizationUsers: { nodes } } },
+const successfulResolver = (nodes, pageInfo = {}) => {
+ return jest.fn().mockResolvedValue({
+ data: { organization: { id: 1, organizationUsers: { nodes, pageInfo } } },
});
+};
const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
describe('OrganizationsUsersApp', () => {
@@ -44,12 +51,13 @@ describe('OrganizationsUsersApp', () => {
const findOrganizationUsersView = () => wrapper.findComponent(OrganizationsUsersView);
describe.each`
- description | mockResolver | loading | userData | error
- ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false}
- ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${false}
- ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false}
- ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true}
- `('$description', ({ mockResolver, loading, userData, error }) => {
+ description | mockResolver | loading | userData | pageInfo | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${{}} | ${false}
+ ${'when API returns successful with one page of results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${{}} | ${false}
+ ${'when API returns successful with multiple pages of results'} | ${successfulResolver(MOCK_USERS, MOCK_PAGE_INFO)} | ${false} | ${MOCK_USERS_FORMATTED} | ${MOCK_PAGE_INFO} | ${false}
+ ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${{}} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${{}} | ${true}
+ `('$description', ({ mockResolver, loading, userData, pageInfo, error }) => {
beforeEach(async () => {
createComponent(mockResolver);
await waitForPromises();
@@ -63,6 +71,10 @@ describe('OrganizationsUsersApp', () => {
expect(findOrganizationUsersView().props('users')).toStrictEqual(userData);
});
+ it('renders OrganizationUsersView with correct pageInfo prop', () => {
+ expect(findOrganizationUsersView().props('pageInfo')).toStrictEqual(pageInfo);
+ });
+
it(`does ${error ? '' : 'not '}render an error message`, () => {
return error
? expect(createAlert).toHaveBeenCalledWith({
@@ -74,4 +86,40 @@ describe('OrganizationsUsersApp', () => {
: expect(createAlert).not.toHaveBeenCalled();
});
});
+
+ describe('Pagination', () => {
+ const mockResolver = successfulResolver(MOCK_USERS, MOCK_PAGE_INFO);
+
+ beforeEach(async () => {
+ createComponent(mockResolver);
+ await waitForPromises();
+ mockResolver.mockClear();
+ });
+
+ it('handleNextPage calls organizationUsersQuery with correct pagination data', async () => {
+ findOrganizationUsersView().vm.$emit('next');
+ await waitForPromises();
+
+ expect(mockResolver).toHaveBeenCalledWith({
+ id: MOCK_ORGANIZATION_GID,
+ before: '',
+ after: MOCK_PAGE_INFO.endCursor,
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ });
+ });
+
+ it('handlePrevPage calls organizationUsersQuery with correct pagination data', async () => {
+ findOrganizationUsersView().vm.$emit('prev');
+ await waitForPromises();
+
+ expect(mockResolver).toHaveBeenCalledWith({
+ id: MOCK_ORGANIZATION_GID,
+ before: MOCK_PAGE_INFO.startCursor,
+ after: '',
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ });
+ });
+ });
});
diff --git a/spec/frontend/organizations/users/components/users_view_spec.js b/spec/frontend/organizations/users/components/users_view_spec.js
index 5f47e18edd8..d665c60d425 100644
--- a/spec/frontend/organizations/users/components/users_view_spec.js
+++ b/spec/frontend/organizations/users/components/users_view_spec.js
@@ -1,8 +1,8 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UsersView from '~/organizations/users/components/users_view.vue';
import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
-import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data';
+import { MOCK_PATHS, MOCK_USERS_FORMATTED, MOCK_PAGE_INFO } from '../mock_data';
describe('UsersView', () => {
let wrapper;
@@ -12,6 +12,7 @@ describe('UsersView', () => {
propsData: {
loading: false,
users: MOCK_USERS_FORMATTED,
+ pageInfo: MOCK_PAGE_INFO,
...props,
},
provide: {
@@ -22,6 +23,7 @@ describe('UsersView', () => {
const findGlLoading = () => wrapper.findComponent(GlLoadingIcon);
const findUsersTable = () => wrapper.findComponent(UsersTable);
+ const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
describe.each`
description | loading | usersData
@@ -40,5 +42,27 @@ describe('UsersView', () => {
it(`does ${!loading ? '' : 'not '}render users table`, () => {
expect(findUsersTable().exists()).toBe(!loading);
});
+
+ it(`does ${!loading ? '' : 'not '}render pagination`, () => {
+ expect(findGlKeysetPagination().exists()).toBe(Boolean(!loading));
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('@next event forwards up to the parent component', () => {
+ findGlKeysetPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next')).toHaveLength(1);
+ });
+
+ it('@prev event forwards up to the parent component', () => {
+ findGlKeysetPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev')).toHaveLength(1);
+ });
});
});
diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js
index b6ca00bed79..16b3ec3bbcb 100644
--- a/spec/frontend/organizations/users/mock_data.js
+++ b/spec/frontend/organizations/users/mock_data.js
@@ -40,3 +40,11 @@ export const MOCK_USERS = [
export const MOCK_USERS_FORMATTED = MOCK_USERS.map(({ badges, user }) => {
return { ...user, badges, email: user.publicEmail };
});
+
+export const MOCK_PAGE_INFO = {
+ startCursor: 'aaaa',
+ endCursor: 'bbbb',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ __typename: 'PageInfo',
+};
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index be50858bc88..3db77469d6b 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -1,16 +1,23 @@
import { GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { getParameterValues } from '~/lib/utils/url_utility';
+
+import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterValues: jest.fn().mockReturnValue([]),
+}));
describe('BulkImportsHistoryApp', () => {
- const API_URL = '/api/v4/bulk_imports/entities';
+ const BULK_IMPORTS_API_URL = '/api/v4/bulk_imports/entities';
const DEFAULT_HEADERS = {
'x-page': 1,
@@ -73,14 +80,14 @@ describe('BulkImportsHistoryApp', () => {
}
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findPaginationBar = () => wrapper.findComponent(PaginationBar);
beforeEach(() => {
gon.api_version = 'v4';
- });
- beforeEach(() => {
+ getParameterValues.mockReturnValue([]);
mock = new MockAdapter(axios);
- mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
});
afterEach(() => {
@@ -94,9 +101,9 @@ describe('BulkImportsHistoryApp', () => {
});
it('renders empty state when no data is available', async () => {
- mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
+ mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
createComponent();
- await axios.waitForAll();
+ await waitForPromises();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
@@ -104,7 +111,7 @@ describe('BulkImportsHistoryApp', () => {
it('renders table with data when history is available', async () => {
createComponent();
- await axios.waitForAll();
+ await waitForPromises();
const table = wrapper.findComponent(GlTableLite);
expect(table.exists()).toBe(true);
@@ -116,26 +123,46 @@ describe('BulkImportsHistoryApp', () => {
const NEW_PAGE = 4;
createComponent();
- await axios.waitForAll();
+ await waitForPromises();
mock.resetHistory();
- wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
- await axios.waitForAll();
+ findPaginationBar().vm.$emit('set-page', NEW_PAGE);
+ await waitForPromises();
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
});
});
+ describe('when filtering by bulk_import_id param', () => {
+ const mockId = 2;
+
+ beforeEach(() => {
+ getParameterValues.mockReturnValue([mockId]);
+ });
+
+ it('makes a request to bulk_import_history endpoint', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].url).toBe(`/api/v4/bulk_imports/${mockId}/entities`);
+ expect(mock.history.get[0].params).toStrictEqual({
+ page: 1,
+ per_page: 20,
+ });
+ });
+ });
+
it('changes page size when requested by pagination bar', async () => {
const NEW_PAGE_SIZE = 4;
createComponent();
- await axios.waitForAll();
+ await waitForPromises();
mock.resetHistory();
- wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
- await axios.waitForAll();
+ findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await waitForPromises();
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].params).toStrictEqual(
@@ -146,15 +173,14 @@ describe('BulkImportsHistoryApp', () => {
it('resets page to 1 when page size is changed', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
- await axios.waitForAll();
- wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
- await axios.waitForAll();
+ await waitForPromises();
+ findPaginationBar().vm.$emit('set-page', 2);
+ await waitForPromises();
mock.resetHistory();
- wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
- await axios.waitForAll();
+ findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await waitForPromises();
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].params).toStrictEqual(
@@ -166,18 +192,18 @@ describe('BulkImportsHistoryApp', () => {
const NEW_PAGE_SIZE = 4;
createComponent();
- await axios.waitForAll();
+ await waitForPromises();
mock.resetHistory();
- wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
- await axios.waitForAll();
+ findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await waitForPromises();
expect(findLocalStorageSync().props('value')).toBe(NEW_PAGE_SIZE);
});
it('renders link to destination_full_path for destination group', async () => {
createComponent({ shallow: false });
- await axios.waitForAll();
+ await waitForPromises();
expect(wrapper.find('tbody tr a').attributes().href).toBe(
`/${DUMMY_RESPONSE[0].destination_full_path}`,
@@ -187,9 +213,9 @@ describe('BulkImportsHistoryApp', () => {
it('renders destination as text when destination_full_path is not defined', async () => {
const RESPONSE = [{ ...DUMMY_RESPONSE[0], destination_full_path: null }];
- mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
- await axios.waitForAll();
+ await waitForPromises();
expect(wrapper.find('tbody tr a').exists()).toBe(false);
expect(wrapper.find('tbody tr span').text()).toBe(
@@ -199,14 +225,14 @@ describe('BulkImportsHistoryApp', () => {
it('adds slash to group urls', async () => {
createComponent({ shallow: false });
- await axios.waitForAll();
+ await waitForPromises();
expect(wrapper.find('tbody tr a').text()).toBe(`${DUMMY_RESPONSE[0].destination_full_path}/`);
});
it('does not prefixes project urls with slash', async () => {
createComponent({ shallow: false });
- await axios.waitForAll();
+ await waitForPromises();
expect(wrapper.findAll('tbody tr a').at(1).text()).toBe(
DUMMY_RESPONSE[1].destination_full_path,
@@ -215,9 +241,9 @@ describe('BulkImportsHistoryApp', () => {
describe('details button', () => {
beforeEach(() => {
- mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
- return axios.waitForAll();
+ return waitForPromises();
});
it('renders details button if relevant item has failures', () => {
@@ -255,7 +281,7 @@ describe('BulkImportsHistoryApp', () => {
createComponent({ shallow: false });
await waitForPromises();
- expect(mock.history.get.map((x) => x.url)).toEqual([API_URL]);
+ expect(mock.history.get.map((x) => x.url)).toEqual([BULK_IMPORTS_API_URL]);
});
});
@@ -279,7 +305,7 @@ describe('BulkImportsHistoryApp', () => {
const RESPONSE = [mockCreatedImport, ...DUMMY_RESPONSE];
const POLL_HEADERS = { 'poll-interval': pollInterval };
- mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
mock.onGet(mockRealtimeChangesPath).replyOnce(HTTP_STATUS_OK, [], POLL_HEADERS);
mock
.onGet(mockRealtimeChangesPath)
@@ -293,7 +319,10 @@ describe('BulkImportsHistoryApp', () => {
it('starts polling for realtime changes', () => {
jest.advanceTimersByTime(pollInterval);
- expect(mock.history.get.map((x) => x.url)).toEqual([API_URL, mockRealtimeChangesPath]);
+ expect(mock.history.get.map((x) => x.url)).toEqual([
+ BULK_IMPORTS_API_URL,
+ mockRealtimeChangesPath,
+ ]);
expect(wrapper.findAll('tbody tr').at(0).text()).toContain('Pending');
});
@@ -305,7 +334,7 @@ describe('BulkImportsHistoryApp', () => {
await waitForPromises();
expect(mock.history.get.map((x) => x.url)).toEqual([
- API_URL,
+ BULK_IMPORTS_API_URL,
mockRealtimeChangesPath,
mockRealtimeChangesPath,
]);
diff --git a/spec/frontend_integration/fly_out_nav_browser_spec.js b/spec/frontend_integration/fly_out_nav_browser_spec.js
deleted file mode 100644
index 07ddc0220e6..00000000000
--- a/spec/frontend_integration/fly_out_nav_browser_spec.js
+++ /dev/null
@@ -1,366 +0,0 @@
-import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
-import {
- calculateTop,
- showSubLevelItems,
- canShowSubItems,
- canShowActiveSubItems,
- mouseEnterTopItems,
- mouseLeaveTopItem,
- getOpenMenu,
- setOpenMenu,
- mousePos,
- getHideSubItemsInterval,
- documentMouseMove,
- getHeaderHeight,
- setSidebar,
- subItemsMouseLeave,
-} from '~/fly_out_nav';
-
-describe('Fly out sidebar navigation', () => {
- let el;
- let breakpointSize = 'lg';
-
- const OLD_SIDEBAR_WIDTH = 200;
- const CONTAINER_INITIAL_BOUNDING_RECT = {
- x: 8,
- y: 8,
- width: 769,
- height: 0,
- top: 8,
- right: 777,
- bottom: 8,
- left: 8,
- };
- const SUB_ITEMS_INITIAL_BOUNDING_RECT = {
- x: 148,
- y: 8,
- width: 0,
- height: 150,
- top: 8,
- right: 148,
- bottom: 158,
- left: 148,
- };
- const mockBoundingClientRect = (elem, rect) => {
- jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect);
- };
-
- const findSubItems = () => document.querySelector('.sidebar-sub-level-items');
- const mockBoundingRects = () => {
- const subItems = findSubItems();
- mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT);
- mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT);
- };
- const mockSidebarFragment = (styleProps = '') =>
- `<div class="sidebar-sub-level-items" style="${styleProps}"></div>`;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.style.position = 'relative';
- document.body.appendChild(el);
-
- jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize);
- });
-
- afterEach(() => {
- document.body.innerHTML = '';
- breakpointSize = 'lg';
- mousePos.length = 0;
-
- setSidebar(null);
- });
-
- describe('calculateTop', () => {
- it('returns boundingRect top', () => {
- const boundingRect = {
- top: 100,
- height: 100,
- };
-
- expect(calculateTop(boundingRect, 100)).toBe(100);
- });
- });
-
- describe('getHideSubItemsInterval', () => {
- beforeEach(() => {
- el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;');
- mockBoundingRects();
- });
-
- it('returns 0 if currentOpenMenu is nil', () => {
- setOpenMenu(null);
- expect(getHideSubItemsInterval()).toBe(0);
- });
-
- it('returns 0 if mousePos is empty', () => {
- expect(getHideSubItemsInterval()).toBe(0);
- });
-
- it('returns 0 when mouse above sub-items', () => {
- showSubLevelItems(el);
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top,
- });
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top - 50,
- });
-
- expect(getHideSubItemsInterval()).toBe(0);
- });
-
- it('returns 0 when mouse is below sub-items', () => {
- const subItems = findSubItems();
-
- showSubLevelItems(el);
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top,
- });
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50,
- });
-
- expect(getHideSubItemsInterval()).toBe(0);
- });
-
- it('returns 300 when mouse is moved towards sub-items', () => {
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top,
- });
-
- showSubLevelItems(el);
- documentMouseMove({
- clientX: el.getBoundingClientRect().left + 20,
- clientY: el.getBoundingClientRect().top + 10,
- });
-
- expect(getHideSubItemsInterval()).toBe(300);
- });
- });
-
- describe('mouseLeaveTopItem', () => {
- beforeEach(() => {
- jest.spyOn(el.classList, 'remove');
- });
-
- it('removes is-over class if currentOpenMenu is null', () => {
- setOpenMenu(null);
-
- mouseLeaveTopItem(el);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-over');
- });
-
- it('removes is-over class if currentOpenMenu is null & there are sub-items', () => {
- setOpenMenu(null);
- el.innerHTML = mockSidebarFragment('position: absolute');
-
- mouseLeaveTopItem(el);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-over');
- });
-
- it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => {
- setOpenMenu(null);
- el.innerHTML = mockSidebarFragment('position: absolute');
-
- setOpenMenu(findSubItems());
- mouseLeaveTopItem(el);
-
- expect(el.classList.remove).not.toHaveBeenCalled();
- });
- });
-
- describe('mouseEnterTopItems', () => {
- beforeEach(() => {
- el.innerHTML = mockSidebarFragment(
- `position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`,
- );
- mockBoundingRects();
- });
-
- it('shows sub-items after 0ms if no menu is open', () => {
- const subItems = findSubItems();
- mouseEnterTopItems(el);
-
- expect(getHideSubItemsInterval()).toBe(0);
-
- return new Promise((resolve) => {
- setTimeout(() => {
- expect(subItems.style.display).toBe('block');
- resolve();
- });
- });
- });
-
- it('shows sub-items after 300ms if a menu is currently open', () => {
- const subItems = findSubItems();
-
- documentMouseMove({
- clientX: el.getBoundingClientRect().left,
- clientY: el.getBoundingClientRect().top,
- });
-
- setOpenMenu(subItems);
-
- documentMouseMove({
- clientX: el.getBoundingClientRect().left + 20,
- clientY: el.getBoundingClientRect().top + 10,
- });
-
- mouseEnterTopItems(el, 0);
-
- return new Promise((resolve) => {
- setTimeout(() => {
- expect(subItems.style.display).toBe('block');
- resolve();
- });
- });
- });
- });
-
- describe('showSubLevelItems', () => {
- beforeEach(() => {
- el.innerHTML = mockSidebarFragment('position: absolute');
- });
-
- it('adds is-over class to el', () => {
- jest.spyOn(el.classList, 'add');
-
- showSubLevelItems(el);
-
- expect(el.classList.add).toHaveBeenCalledWith('is-over');
- });
-
- it('does not show sub-items on mobile', () => {
- breakpointSize = 'xs';
-
- showSubLevelItems(el);
-
- expect(findSubItems().style.display).not.toBe('block');
- });
-
- it('shows sub-items', () => {
- showSubLevelItems(el);
-
- expect(findSubItems().style.display).toBe('block');
- });
-
- it('shows collapsed only sub-items if icon only sidebar', () => {
- const subItems = findSubItems();
- const sidebar = document.createElement('div');
- sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
- subItems.classList.add('is-fly-out-only');
-
- setSidebar(sidebar);
-
- showSubLevelItems(el);
-
- expect(findSubItems().style.display).toBe('block');
- });
-
- it('does not show collapsed only sub-items if icon only sidebar', () => {
- const subItems = findSubItems();
- subItems.classList.add('is-fly-out-only');
-
- showSubLevelItems(el);
-
- expect(subItems.style.display).not.toBe('block');
- });
-
- it('sets transform of sub-items', () => {
- const sidebar = document.createElement('div');
- const subItems = findSubItems();
-
- sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`;
-
- document.body.appendChild(sidebar);
-
- setSidebar(sidebar);
- showSubLevelItems(el);
-
- expect(subItems.style.transform).toBe(
- `translate3d(${OLD_SIDEBAR_WIDTH}px, ${
- Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()
- }px, 0)`,
- );
- });
-
- it('sets is-above when element is above', () => {
- const subItems = findSubItems();
- mockBoundingRects();
-
- subItems.style.height = `${window.innerHeight + el.offsetHeight}px`;
- el.style.top = `${window.innerHeight - el.offsetHeight}px`;
-
- jest.spyOn(subItems.classList, 'add');
-
- showSubLevelItems(el);
-
- expect(subItems.classList.add).toHaveBeenCalledWith('is-above');
- });
- });
-
- describe('canShowSubItems', () => {
- it('returns true if on desktop size', () => {
- expect(canShowSubItems()).toBe(true);
- });
-
- it('returns false if on mobile size', () => {
- breakpointSize = 'xs';
-
- expect(canShowSubItems()).toBe(false);
- });
- });
-
- describe('canShowActiveSubItems', () => {
- it('returns true by default', () => {
- expect(canShowActiveSubItems(el)).toBe(true);
- });
-
- it('returns false when active & expanded sidebar', () => {
- const sidebar = document.createElement('div');
- el.classList.add('active');
-
- setSidebar(sidebar);
-
- expect(canShowActiveSubItems(el)).toBe(false);
- });
-
- it('returns true when active & collapsed sidebar', () => {
- const sidebar = document.createElement('div');
- sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
- el.classList.add('active');
-
- setSidebar(sidebar);
-
- expect(canShowActiveSubItems(el)).toBe(true);
- });
- });
-
- describe('subItemsMouseLeave', () => {
- beforeEach(() => {
- el.innerHTML = mockSidebarFragment('position: absolute');
-
- setOpenMenu(findSubItems());
- });
-
- it('hides subMenu if element is not hovered', () => {
- subItemsMouseLeave(el);
-
- expect(getOpenMenu()).toBeNull();
- });
-
- it('does not hide subMenu if element is hovered', () => {
- el.classList.add('is-over');
- subItemsMouseLeave(el);
-
- expect(getOpenMenu()).not.toBeNull();
- });
- });
-});
diff --git a/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb
new file mode 100644
index 00000000000..1ce0e91765f
--- /dev/null
+++ b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::Catalog::Resources::VersionsResolver, feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ include_context 'when there are catalog resources with versions'
+
+ let(:sort) { nil }
+ let(:args) { { sort: sort }.compact }
+ let(:ctx) { { current_user: current_user } }
+
+ subject(:result) { resolve(described_class, ctx: ctx, obj: resource1, args: args) }
+
+ describe '#resolve' do
+ context 'when the user is authorized to read project releases' do
+ before_all do
+ resource1.project.add_guest(current_user)
+ end
+
+ context 'when sort argument is not provided' do
+ it 'returns versions ordered by released_at descending' do
+ expect(result.items).to eq([v1_1, v1_0])
+ end
+ end
+
+ context 'when sort argument is provided' do
+ context 'when sort is CREATED_ASC' do
+ let(:sort) { 'CREATED_ASC' }
+
+ it 'returns versions ordered by created_at ascending' do
+ expect(result.items.to_a).to eq([v1_1, v1_0])
+ end
+ end
+
+ context 'when sort is CREATED_DESC' do
+ let(:sort) { 'CREATED_DESC' }
+
+ it 'returns versions ordered by created_at descending' do
+ expect(result.items).to eq([v1_0, v1_1])
+ end
+ end
+
+ context 'when sort is RELEASED_AT_ASC' do
+ let(:sort) { 'RELEASED_AT_ASC' }
+
+ it 'returns versions ordered by released_at ascending' do
+ expect(result.items).to eq([v1_0, v1_1])
+ end
+ end
+
+ context 'when sort is RELEASED_AT_DESC' do
+ let(:sort) { 'RELEASED_AT_DESC' }
+
+ it 'returns versions ordered by released_at descending' do
+ expect(result.items).to eq([v1_1, v1_0])
+ end
+ end
+ end
+ end
+
+ context 'when the user is not authorized to read project releases' do
+ it 'returns empty response' do
+ expect(result).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
deleted file mode 100644
index 02fb3dfaee4..00000000000
--- a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# In this context, a `version` is equivalent to a `release`
-RSpec.describe Resolvers::Ci::Catalog::VersionsResolver, feature_category: :pipeline_composition do
- include GraphqlHelpers
-
- let_it_be(:today) { Time.now }
- let_it_be(:yesterday) { today - 1.day }
- let_it_be(:tomorrow) { today + 1.day }
-
- let_it_be(:project) { create(:project, :private) }
- # rubocop: disable Layout/LineLength
- let_it_be(:version1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) }
- let_it_be(:version2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) }
- let_it_be(:version3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) }
- # rubocop: enable Layout/LineLength
- let_it_be(:developer) { create(:user) }
- let_it_be(:public_user) { create(:user) }
-
- let(:args) { { sort: :released_at_desc } }
- let(:all_releases) { [version1, version2, version3] }
-
- before_all do
- project.add_developer(developer)
- end
-
- describe '#resolve' do
- it_behaves_like 'releases and group releases resolver'
-
- describe 'when order_by is created_at' do
- let(:current_user) { developer }
-
- context 'with sort: desc' do
- let(:args) { { sort: :created_desc } }
-
- it 'returns the releases ordered by created_at in descending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:created_at, :desc)
- end
- end
-
- context 'with sort: asc' do
- let(:args) { { sort: :created_asc } }
-
- it 'returns the releases ordered by created_at in ascending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:created_at, :asc)
- end
- end
- end
- end
-
- private
-
- def resolve_versions
- context = { current_user: current_user }
- resolve(described_class, obj: project, args: args, ctx: context, arg_style: :internal)
- end
-
- # Required for shared examples
- alias_method :resolve_releases, :resolve_versions
-end
diff --git a/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb
new file mode 100644
index 00000000000..fd0f1a1e553
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiCatalogResourceVersionSort'], feature_category: :pipeline_composition do
+ it { expect(described_class.graphql_name).to eq('CiCatalogResourceVersionSort') }
+
+ it 'exposes all the existing catalog resource version sort options' do
+ expect(described_class.values.keys).to include(
+ *%w[RELEASED_AT_ASC RELEASED_AT_DESC CREATED_ASC CREATED_DESC]
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/catalog/resources/version_type_spec.rb b/spec/graphql/types/ci/catalog/resources/version_type_spec.rb
new file mode 100644
index 00000000000..9faf3f16313
--- /dev/null
+++ b/spec/graphql/types/ci/catalog/resources/version_type_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Catalog::Resources::VersionType, feature_category: :pipeline_composition do
+ specify { expect(described_class.graphql_name).to eq('CiCatalogResourceVersion') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ id
+ created_at
+ released_at
+ tag_name
+ tag_path
+ author
+ commit
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
index 338475fa9c4..648213dc152 100644
--- a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
+++ b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb
@@ -11,7 +11,9 @@ RSpec.describe 'new tables with gitlab_main schema', feature_category: :cell do
# Specific tables can be exempted from this requirement, and such tables must be added to the `exempted_tables` list.
let!(:exempted_tables) do
- []
+ [
+ "audit_events_instance_amazon_s3_configurations" # https://gitlab.com/gitlab-org/gitlab/-/issues/431327
+ ]
end
let!(:starting_from_milestone) { 16.7 }
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 775695b406c..53ad3a3a698 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -36,6 +36,24 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
expect(bridge).to have_one(:downstream_pipeline)
end
+ describe 'no-op methods for compatibility with Ci::Build' do
+ it 'returns an empty array job_artifacts' do
+ expect(bridge.job_artifacts).to eq(Ci::JobArtifact.none)
+ end
+
+ it 'return nil for artifacts_expire_at' do
+ expect(bridge.artifacts_expire_at).to be_nil
+ end
+
+ it 'return nil for runner' do
+ expect(bridge.runner).to be_nil
+ end
+
+ it 'returns an empty TagList for tag_list' do
+ expect(bridge.tag_list).to be_a(ActsAsTaggableOn::TagList)
+ end
+ end
+
describe '#retryable?' do
let(:bridge) { create(:ci_bridge, :success) }
diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb
index ccc729ee715..aafd51699b5 100644
--- a/spec/models/ci/catalog/resources/version_spec.rb
+++ b/spec/models/ci/catalog/resources/version_spec.rb
@@ -10,9 +10,6 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category:
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') }
- it { is_expected.to delegate_method(:name).to(:release) }
- it { is_expected.to delegate_method(:description).to(:release) }
- it { is_expected.to delegate_method(:tag).to(:release) }
it { is_expected.to delegate_method(:sha).to(:release) }
it { is_expected.to delegate_method(:released_at).to(:release) }
it { is_expected.to delegate_method(:author_id).to(:release) }
@@ -127,4 +124,28 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category:
end
end
end
+
+ describe '#name' do
+ it 'is equivalent to release.tag' do
+ release_v1_0.update!(name: 'Release v1.0')
+
+ expect(v1_0.name).to eq(release_v1_0.tag)
+ end
+ end
+
+ describe '#commit' do
+ subject(:commit) { v1_0.commit }
+
+ it 'returns a commit' do
+ is_expected.to be_a(Commit)
+ end
+
+ context 'when the sha is nil' do
+ it 'returns nil' do
+ release_v1_0.update!(sha: nil)
+
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 41c5847e940..d9248ad6855 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -161,24 +161,56 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/deployments/:deployment_id' do
- let(:project) { deployment.environment.project }
- let!(:deployment) { create(:deployment, :success) }
+ let_it_be(:deployment_with_bridge) { create(:deployment, :with_bridge, :success) }
+ let_it_be(:deployment_with_build) { create(:deployment, :success) }
context 'as a member of the project' do
- it 'returns the projects deployment' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+ shared_examples "returns project deployments" do
+ let(:project) { deployment.environment.project }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['sha']).to match /\A\h{40}\z/
- expect(json_response['id']).to eq(deployment.id)
+ it 'returns the expected response' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ expect(json_response['id']).to eq(deployment.id)
+ end
+ end
+
+ context 'when the deployable is a build' do
+ it_behaves_like 'returns project deployments' do
+ let!(:deployment) { deployment_with_build }
+ end
+ end
+
+ context 'when the deployable is a bridge' do
+ it_behaves_like 'returns project deployments' do
+ let!(:deployment) { deployment_with_bridge }
+ end
end
end
context 'as non member' do
- it 'returns a 404 status code' do
- get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+ shared_examples 'deployment will not be found' do
+ let(:project) { deployment.environment.project }
- expect(response).to have_gitlab_http_status(:not_found)
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the deployable is a build' do
+ it_behaves_like 'deployment will not be found' do
+ let!(:deployment) { deployment_with_build }
+ end
+ end
+
+ context 'when the deployable is a bridge' do
+ it_behaves_like 'deployment will not be found' do
+ let!(:deployment) { deployment_with_bridge }
+ end
end
end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 498e030da0b..aed97bcfe7c 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -374,32 +374,71 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/environments/:environment_id' do
+ let_it_be(:bridge_job) { create(:ci_bridge, :running, project: project, user: user) }
+ let_it_be(:build_job) { create(:ci_build, :running, project: project, user: user) }
+
context 'as member of the project' do
- it 'returns project environments' do
- create(:deployment, :success, project: project, environment: environment)
+ shared_examples "returns project environments" do
+ it 'returns expected response' do
+ create(
+ :deployment,
+ :success,
+ project: project,
+ environment: environment,
+ deployable: job
+ )
+
+ get api("/projects/#{project.id}/environments/#{environment.id}", user)
- get api("/projects/#{project.id}/environments/#{environment.id}", user)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/environment')
+ expect(json_response['last_deployment']).to be_present
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/environment')
- expect(json_response['last_deployment']).to be_present
+ context "when the deployable is a bridge" do
+ it_behaves_like "returns project environments" do
+ let(:job) { bridge_job }
+ end
+
+ # No test for Ci::Bridge JOB-TOKEN auth because it doesn't implement the `.token` method.
end
- it 'returns 200 HTTP status when using JOB-TOKEN auth' do
- job = create(:ci_build, :running, project: project, user: user)
+ context "when the deployable is a build" do
+ it_behaves_like "returns project environments" do
+ let(:job) { build_job }
+ end
- get api("/projects/#{project.id}/environments/#{environment.id}"),
- params: { job_token: job.token }
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ get(
+ api("/projects/#{project.id}/environments/#{environment.id}"),
+ params: { job_token: build_job.token }
+ )
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
context 'as non member' do
- it 'returns a 404 status code' do
- get api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+ shared_examples 'environment will not be found' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments/#{environment.id}", non_member)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when the deployable is a bridge" do
+ it_behaves_like "environment will not be found" do
+ let(:job) { bridge_job }
+ end
+ end
+
+ context "when the deployable is a build" do
+ it_behaves_like "environment will not be found" do
+ let(:job) { build_job }
+ end
end
end
end
diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
index 56c6c341647..e9610e3c435 100644
--- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb
+++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb
@@ -81,6 +81,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
nodes {
id
tagName
+ tagPath
releasedAt
author {
id
@@ -98,11 +99,13 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
let_it_be(:author) { create(:user, name: 'author') }
let_it_be(:version1) do
- create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z',
+ author: author).catalog_resource_version
end
let_it_be(:version2) do
- create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z',
+ author: author).catalog_resource_version
end
it 'returns the resource with the versions data' do
@@ -115,13 +118,15 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly(
a_graphql_entity_for(
version1,
- tagName: version1.tag,
+ tagName: version1.name,
+ tagPath: project_tag_path(project, version1.name),
releasedAt: version1.released_at,
author: a_graphql_entity_for(author, :name)
),
a_graphql_entity_for(
version2,
- tagName: version2.tag,
+ tagName: version2.name,
+ tagPath: project_tag_path(project, version2.name),
releasedAt: version2.released_at,
author: a_graphql_entity_for(author, :name)
)
@@ -157,6 +162,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
latestVersion {
id
tagName
+ tagPath
releasedAt
author {
id
@@ -173,12 +179,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
let_it_be(:author) { create(:user, name: 'author') }
let_it_be(:latest_version) do
- create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author)
+ create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z',
+ author: author).catalog_resource_version
end
before_all do
- # Previous version of the project
- create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author)
+ # Previous version of the catalog resource
+ create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z',
+ author: author)
end
it 'returns the resource with the latest version data' do
@@ -189,7 +197,8 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio
resource,
latestVersion: a_graphql_entity_for(
latest_version,
- tagName: latest_version.tag,
+ tagName: latest_version.name,
+ tagPath: project_tag_path(project, latest_version.name),
releasedAt: latest_version.released_at,
author: a_graphql_entity_for(author, :name)
)
diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
index 35f72f8b10f..dee841898bb 100644
--- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb
+++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb
@@ -133,11 +133,13 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi
let_it_be(:author2) { create(:user, name: 'author2') }
let_it_be(:latest_version1) do
- create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1)
+ create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-02-01T00:00:00Z',
+ author: author1).catalog_resource_version
end
let_it_be(:latest_version2) do
- create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2)
+ create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-02-01T00:00:00Z',
+ author: author2).catalog_resource_version
end
let(:query) do
@@ -165,9 +167,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi
before_all do
namespace.add_developer(user)
- # Previous versions of the projects
- create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1)
- create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2)
+ # Previous versions of the catalog resources
+ create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-01-01T00:00:00Z',
+ author: author1)
+ create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-01-01T00:00:00Z',
+ author: author2)
end
it 'returns all resources with the latest version data' do
@@ -178,7 +182,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi
resource1,
latestVersion: a_graphql_entity_for(
latest_version1,
- tagName: latest_version1.tag,
+ tagName: latest_version1.name,
releasedAt: latest_version1.released_at,
author: a_graphql_entity_for(author1, :name)
)
@@ -187,7 +191,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi
public_resource,
latestVersion: a_graphql_entity_for(
latest_version2,
- tagName: latest_version2.tag,
+ tagName: latest_version2.name,
releasedAt: latest_version2.released_at,
author: a_graphql_entity_for(author2, :name)
)
@@ -195,8 +199,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi
)
end
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350
- # it_behaves_like 'avoids N+1 queries'
+ it_behaves_like 'avoids N+1 queries'
end
describe 'rootNamespace' do
diff --git a/spec/services/ml/model_versions/delete_service_spec.rb b/spec/services/ml/model_versions/delete_service_spec.rb
new file mode 100644
index 00000000000..1cc5a2f85a5
--- /dev/null
+++ b/spec/services/ml/model_versions/delete_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::ModelVersions::DeleteService, feature_category: :mlops do
+ let_it_be(:valid_model_version) do
+ create(:ml_model_versions, :with_package)
+ end
+
+ let(:project) { valid_model_version.project }
+ let(:user) { valid_model_version.project.owner }
+ let(:name) { valid_model_version.name }
+ let(:version) { valid_model_version.version }
+
+ subject(:execute_service) { described_class.new(project, name, version, user).execute }
+
+ describe '#execute' do
+ context 'when model version exists' do
+ it 'deletes the model version', :aggregate_failures do
+ expect(execute_service).to be_success
+ expect(Ml::ModelVersion.find_by(id: valid_model_version.id)).to be_nil
+ end
+ end
+
+ context 'when model version does not exist' do
+ let(:version) { 'wrong-version' }
+
+ it { is_expected.to be_error.and have_attributes(message: 'Model not found') }
+ end
+
+ context 'when model version has no package' do
+ before do
+ valid_model_version.update!(package: nil)
+ end
+
+ it 'does not trigger destroy package service', :aggregate_failures do
+ expect(Packages::MarkPackageForDestructionService).not_to receive(:new)
+ expect(execute_service).to be_success
+ end
+ end
+
+ context 'when package cannot be marked for destruction' do
+ before do
+ allow_next_instance_of(Packages::MarkPackageForDestructionService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it 'does not delete the model version', :aggregate_failures do
+ is_expected.to be_error.and have_attributes(message: 'error')
+ expect(Ml::ModelVersion.find_by(id: valid_model_version.id)).to eq(valid_model_version)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
index ac2db624a38..3eeaa52d221 100644
--- a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
+++ b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb
@@ -10,7 +10,7 @@ RSpec.shared_context 'when there are catalog resources with versions' do
let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) }
let_it_be(:resource3) { create(:ci_catalog_resource, project: project3) }
- let_it_be(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) }
+ let_it_be_with_reload(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) }
let_it_be(:release_v1_1) { create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago) }
let_it_be(:release_v2_0) { create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) }
let_it_be(:release_v2_1) { create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) }
diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
index 80e2f09ed44..6d4b82730da 100644
--- a/spec/support/shared_examples/services/protected_branches_shared_examples.rb
+++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_context 'with scan result policy blocking protected branches' do
end
let(:scan_result_policy) do
- build(:scan_result_policy, branches: [branch_name], approval_settings: { block_unprotecting_branches: true })
+ build(:scan_result_policy, branches: [branch_name], approval_settings: { block_branch_modification: true })
end
before do