From 6b19945915303e04fcca21405bca0cd94125199c Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 19 Oct 2021 12:12:07 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/review-apps/qa.gitlab-ci.yml | 1 + .../create_merge_request_dropdown.js | 40 ++++- .../javascripts/diffs/components/app.vue | 9 - .../projects/work_items/{index => }/index.js | 0 app/assets/javascripts/project_visibility.js | 64 ++++--- .../new/components/new_project_url_select.vue | 27 ++- ...ere_user_can_create_projects.query.graphql | 3 + .../labels_select_vue/store/mutations.js | 3 +- .../javascripts/work_items/components/app.vue | 10 +- .../javascripts/work_items/constants.js | 3 + .../work_items/graphql/fragmentTypes.json | 1 + .../work_items/graphql/provider.js | 54 ++++++ .../work_items/graphql/resolvers.js | 0 .../work_items/graphql/typedefs.graphql | 38 +++++ .../graphql/widget.fragment.graphql | 3 + .../graphql/work_item.query.graphql | 16 ++ app/assets/javascripts/work_items/index.js | 4 + .../work_items/pages/work_item_root.vue | 44 +++++ .../javascripts/work_items/router/index.js | 14 ++ .../javascripts/work_items/router/routes.js | 8 + app/controllers/application_controller.rb | 3 +- .../concerns/workhorse_authorization.rb | 2 +- app/models/ci/pipeline.rb | 10 ++ app/models/concerns/ci/contextable.rb | 20 ++- app/uploaders/bulk_imports/export_uploader.rb | 2 +- app/uploaders/import_export_uploader.rb | 4 +- app/views/projects/work_items/index.html.haml | 2 +- app/views/shared/_visibility_radios.html.haml | 1 + .../ci_predefined_vars_in_builder.yml | 8 + config/initializers/0_marginalia.rb | 5 +- config/routes/project.rb | 2 +- .../monitoring/prometheus/gitlab_metrics.md | 1 + doc/api/graphql/reference/index.md | 1 + doc/api/integrations.md | 148 ++++++++-------- lib/api/api.rb | 3 +- lib/api/helpers/file_upload_helpers.rb | 2 +- lib/gitlab/ci/pipeline/metrics.rb | 9 + lib/gitlab/ci/variables/builder.rb | 49 ++++++ .../grape_logging/loggers/urgency_logger.rb | 19 +++ lib/gitlab/health_checks/redis/cache_check.rb | 26 +-- .../health_checks/redis/queues_check.rb | 26 +-- .../redis/rate_limiting_check.rb | 26 +-- .../redis/redis_abstract_check.rb | 41 +++++ lib/gitlab/health_checks/redis/redis_check.rb | 20 ++- .../health_checks/redis/sessions_check.rb | 26 +-- .../health_checks/redis/shared_state_check.rb | 26 +-- .../health_checks/redis/trace_chunks_check.rb | 26 +-- lib/gitlab/import_export/command_line_util.rb | 16 +- lib/gitlab/lograge/custom_options.rb | 17 +- lib/gitlab/metrics/rails_slis.rb | 7 +- .../metrics/requests_rack_middleware.rb | 14 +- qa/qa/resource/base.rb | 2 +- qa/qa/resource/project.rb | 4 +- .../api/1_manage/bulk_import_project_spec.rb | 5 +- .../application_controller_spec.rb | 28 ++++ .../create_merge_request_dropdown_spec.js | 5 +- spec/frontend/diffs/components/app_spec.js | 19 --- .../components/new_project_url_select_spec.js | 39 ++++- .../labels_select_vue/store/mutations_spec.js | 10 +- .../work_items/components/app_spec.js | 24 +++ spec/frontend/work_items/mock_data.js | 17 ++ .../work_items/pages/work_item_root_spec.js | 70 ++++++++ spec/frontend/work_items/router_spec.js | 30 ++++ spec/lib/gitlab/ci/variables/builder_spec.rb | 38 +++++ .../loggers/urgency_logger_spec.rb | 48 ++++++ .../health_checks/redis/redis_check_spec.rb | 2 +- .../import_export/command_line_util_spec.rb | 44 +++++ .../lib/gitlab/lograge/custom_options_spec.rb | 20 ++- spec/lib/gitlab/metrics/rails_slis_spec.rb | 6 +- .../metrics/requests_rack_middleware_spec.rb | 62 +++++-- spec/models/ci/build_spec.rb | 158 ++++++++++++------ 71 files changed, 1106 insertions(+), 429 deletions(-) rename app/assets/javascripts/pages/projects/work_items/{index => }/index.js (100%) create mode 100644 app/assets/javascripts/work_items/constants.js create mode 100644 app/assets/javascripts/work_items/graphql/fragmentTypes.json create mode 100644 app/assets/javascripts/work_items/graphql/provider.js create mode 100644 app/assets/javascripts/work_items/graphql/resolvers.js create mode 100644 app/assets/javascripts/work_items/graphql/widget.fragment.graphql create mode 100644 app/assets/javascripts/work_items/graphql/work_item.query.graphql create mode 100644 app/assets/javascripts/work_items/pages/work_item_root.vue create mode 100644 app/assets/javascripts/work_items/router/index.js create mode 100644 app/assets/javascripts/work_items/router/routes.js create mode 100644 config/feature_flags/development/ci_predefined_vars_in_builder.yml create mode 100644 lib/gitlab/ci/variables/builder.rb create mode 100644 lib/gitlab/grape_logging/loggers/urgency_logger.rb create mode 100644 lib/gitlab/health_checks/redis/redis_abstract_check.rb create mode 100644 spec/frontend/work_items/components/app_spec.js create mode 100644 spec/frontend/work_items/mock_data.js create mode 100644 spec/frontend/work_items/pages/work_item_root_spec.js create mode 100644 spec/frontend/work_items/router_spec.js create mode 100644 spec/lib/gitlab/ci/variables/builder_spec.rb create mode 100644 spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 6b9d4feb3c8..6dd0997b3bb 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -14,6 +14,7 @@ GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}" GITLAB_ADMIN_USERNAME: "root" GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}" + GITLAB_QA_ADMIN_ACCESS_TOKEN: "${REVIEW_APPS_ROOT_TOKEN}" GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}" EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}" SIGNUP_DISABLED: "true" diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index f4a27dc7d1f..ae6e6bf02e4 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -54,6 +54,7 @@ export default class CreateMergeRequestDropdown { this.isCreatingBranch = false; this.isCreatingMergeRequest = false; this.isGettingRef = false; + this.refCancelToken = null; this.mergeRequestCreated = false; this.refDebounce = debounce((value, target) => this.getRef(value, target), 500); this.refIsValid = true; @@ -101,9 +102,18 @@ export default class CreateMergeRequestDropdown { 'click', this.onClickCreateMergeRequestButton.bind(this), ); + this.branchInput.addEventListener('input', this.onChangeInput.bind(this)); this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this)); this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this)); + // Detect for example when user pastes ref using the mouse + this.refInput.addEventListener('input', this.onChangeInput.bind(this)); + // Detect for example when user presses right arrow to apply the suggested ref this.refInput.addEventListener('keyup', this.onChangeInput.bind(this)); + // Detect when user clicks inside the input to apply the suggested ref + this.refInput.addEventListener('click', this.onChangeInput.bind(this)); + // Detect when user clicks outside the input to apply the suggested ref + this.refInput.addEventListener('blur', this.onChangeInput.bind(this)); + // Detect when user presses tab to apply the suggested ref this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this)); } @@ -247,8 +257,12 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; + this.refCancelToken = axios.CancelToken.source(); + return axios - .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`) + .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, { + cancelToken: this.refCancelToken.token, + }) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -267,7 +281,10 @@ export default class CreateMergeRequestDropdown { return this.updateInputState(target, ref, result); }) - .catch(() => { + .catch((thrown) => { + if (axios.isCancel(thrown)) { + return false; + } this.unavailable(); this.disable(); createFlash({ @@ -325,14 +342,23 @@ export default class CreateMergeRequestDropdown { let target; let value; + // User changed input, cancel to prevent previous request from interfering + if (this.refCancelToken !== null) { + this.refCancelToken.cancel(); + } + if (event.target === this.branchInput) { target = 'branch'; ({ value } = this.branchInput); } else if (event.target === this.refInput) { target = 'ref'; - value = - event.target.value.slice(0, event.target.selectionStart) + - event.target.value.slice(event.target.selectionEnd); + if (event.target === document.activeElement) { + value = + event.target.value.slice(0, event.target.selectionStart) + + event.target.value.slice(event.target.selectionEnd); + } else { + value = event.target.value; + } } else { return false; } @@ -358,6 +384,7 @@ export default class CreateMergeRequestDropdown { this.enable(); this.showAvailableMessage(target); + this.refDebounce(value, target); return true; } @@ -414,7 +441,8 @@ export default class CreateMergeRequestDropdown { if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return; event.preventDefault(); - window.getSelection().removeAllRanges(); + const caretPositionEnd = this.refInput.value.length; + this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd); } removeMessage(target) { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 465f9836140..cf0d2814136 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -392,8 +392,6 @@ export default { diffsApp.instrument(); }, created() { - this.mergeRequestContainers = document.querySelectorAll('.merge-request-container'); - this.adjustView(); this.subscribeToEvents(); @@ -521,13 +519,6 @@ export default { } else { this.removeEventListeners(); } - - if (!this.isFluidLayout && this.glFeatures.mrChangesFluidLayout) { - this.mergeRequestContainers.forEach((el) => { - el.classList.toggle('limit-container-width', !this.shouldShow); - el.classList.toggle('container-limited', !this.shouldShow); - }); - } }, setEventListeners() { Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1)); diff --git a/app/assets/javascripts/pages/projects/work_items/index/index.js b/app/assets/javascripts/pages/projects/work_items/index.js similarity index 100% rename from app/assets/javascripts/pages/projects/work_items/index/index.js rename to app/assets/javascripts/pages/projects/work_items/index.js diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index e3868e2925d..1b57a69d464 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -1,42 +1,58 @@ import $ from 'jquery'; +import eventHub from '~/projects/new/event_hub'; -function setVisibilityOptions(namespaceSelector) { - if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) { - return; - } - const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; - const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset; +// Values are from lib/gitlab/visibility_level.rb +const visibilityLevel = { + private: 0, + internal: 10, + public: 20, +}; +function setVisibilityOptions({ name, visibility, showPath, editPath }) { document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => { - const optionInput = option.querySelector('input[type=radio]'); - const optionValue = optionInput ? optionInput.value : 0; - const optionTitle = option.querySelector('.option-title'); - const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : ''; + // Don't change anything if the option is restricted by admin + if (option.classList.contains('restricted')) { + return; + } - // don't change anything if the option is restricted by admin - if (!option.classList.contains('restricted')) { - if (visibilityLevel < optionValue) { - option.classList.add('disabled'); - optionInput.disabled = true; - const reason = option.querySelector('.option-disabled-reason'); - if (reason) { - reason.innerHTML = `This project cannot be ${optionName} because the visibility of + const optionInput = option.querySelector('input[type=radio]'); + const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0; + + if (visibilityLevel[visibility] < optionValue) { + option.classList.add('disabled'); + optionInput.disabled = true; + const reason = option.querySelector('.option-disabled-reason'); + if (reason) { + const optionTitle = option.querySelector('.option-title'); + const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : ''; + reason.innerHTML = `This project cannot be ${optionName} because the visibility of ${name} is ${visibility}. To make this project ${optionName}, you must first change the visibility of the parent group.`; - } - } else { - option.classList.remove('disabled'); - optionInput.disabled = false; } + } else { + option.classList.remove('disabled'); + optionInput.disabled = false; } }); } +function handleSelect2DropdownChange(namespaceSelector) { + if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) { + return; + } + const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; + setVisibilityOptions(selectedNamespace.dataset); +} + export default function initProjectVisibilitySelector() { + eventHub.$on('update-visibility', setVisibilityOptions); + const namespaceSelector = document.querySelector('select.js-select-namespace'); if (namespaceSelector) { - $('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector)); - setVisibilityOptions(namespaceSelector); + $('.select2.js-select-namespace').on('change', () => + handleSelect2DropdownChange(namespaceSelector), + ); + handleSelect2DropdownChange(namespaceSelector); } } diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue index bf44ff70562..e0ba60074af 100644 --- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue +++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue @@ -6,9 +6,9 @@ import { GlDropdownItem, GlDropdownText, GlDropdownSectionHeader, - GlLoadingIcon, GlSearchBoxByType, } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import Tracking from '~/tracking'; @@ -24,7 +24,6 @@ export default { GlDropdownItem, GlDropdownText, GlDropdownSectionHeader, - GlLoadingIcon, GlSearchBoxByType, }, mixins: [Tracking.mixin()], @@ -103,6 +102,15 @@ export default { focusInput() { this.$refs.search.focusInput(); }, + handleDropdownItemClick(namespace) { + eventHub.$emit('update-visibility', { + name: namespace.name, + visibility: namespace.visibility, + showPath: namespace.webUrl, + editPath: joinPaths(namespace.webUrl, '-', 'edit'), + }); + this.setNamespace(namespace); + }, handleSelectTemplate(groupId) { this.groupToFilterBy = this.userGroups.find( (group) => getIdFromGraphQLId(group.id) === groupId, @@ -134,23 +142,23 @@ export default { - - diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql index e16fe5dde49..74febec5a51 100644 --- a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql +++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql @@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) { nodes { id fullPath + name + visibility + webUrl } } namespace { diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js index 562ff154001..0ea22eb7aea 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js @@ -69,8 +69,7 @@ export default { if (isScopedLabel(candidateLabel)) { const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`; const currentActiveScopedLabel = state.labels.find( - ({ set, title }) => - set && title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title, + ({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title, ); if (currentActiveScopedLabel) { diff --git a/app/assets/javascripts/work_items/components/app.vue b/app/assets/javascripts/work_items/components/app.vue index 93de17d1e43..a14d0c32cbe 100644 --- a/app/assets/javascripts/work_items/components/app.vue +++ b/app/assets/javascripts/work_items/components/app.vue @@ -1,9 +1,5 @@ - - diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js new file mode 100644 index 00000000000..b39f68abf74 --- /dev/null +++ b/app/assets/javascripts/work_items/constants.js @@ -0,0 +1,3 @@ +export const widgetTypes = { + title: 'TITLE', +}; diff --git a/app/assets/javascripts/work_items/graphql/fragmentTypes.json b/app/assets/javascripts/work_items/graphql/fragmentTypes.json new file mode 100644 index 00000000000..c048ac34ac0 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/fragmentTypes.json @@ -0,0 +1 @@ +{"__schema":{"types":[{"kind":"INTERFACE","name":"WorkItemWidget","possibleTypes":[{"name":"TitleWidget"}]}]}} diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js new file mode 100644 index 00000000000..dae663433a6 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/provider.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import createDefaultClient from '~/lib/graphql'; +import workItemQuery from './work_item.query.graphql'; +import introspectionQueryResultData from './fragmentTypes.json'; + +const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData, +}); + +export function createApolloProvider() { + Vue.use(VueApollo); + + const defaultClient = createDefaultClient( + {}, + { + cacheConfig: { + fragmentMatcher, + }, + assumeImmutableResults: true, + }, + ); + + defaultClient.cache.writeQuery({ + query: workItemQuery, + variables: { + id: '1', + }, + data: { + workItem: { + __typename: 'WorkItem', + id: '1', + type: 'FEATURE', + widgets: { + __typename: 'WorkItemWidgetConnection', + nodes: [ + { + __typename: 'TitleWidget', + type: 'TITLE', + enabled: true, + // eslint-disable-next-line @gitlab/require-i18n-strings + contentText: 'Test', + }, + ], + }, + }, + }, + }); + + return new VueApollo({ + defaultClient, + }); +} diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql index e69de29bb2d..4a6e4aeed60 100644 --- a/app/assets/javascripts/work_items/graphql/typedefs.graphql +++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql @@ -0,0 +1,38 @@ +enum WorkItemType { + FEATURE +} + +enum WidgetType { + TITLE +} + +interface WorkItemWidget { + type: WidgetType! +} + +# Replicating Relay connection type for client schema +type WorkItemWidgetEdge { + cursor: String! + node: WorkItemWidget +} + +type WorkItemWidgetConnection { + edges: [WorkItemWidgetEdge] + nodes: [WorkItemWidget] + pageInfo: PageInfo! +} + +type TitleWidget implements WorkItemWidget { + type: WidgetType! + contentText: String! +} + +type WorkItem { + id: ID! + type: WorkItemType! + widgets: [WorkItemWidgetConnection] +} + +extend type Query { + workItem(id: ID!): WorkItem! +} diff --git a/app/assets/javascripts/work_items/graphql/widget.fragment.graphql b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql new file mode 100644 index 00000000000..d7608c26052 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql @@ -0,0 +1,3 @@ +fragment WidgetBase on WorkItemWidget { + type +} diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql new file mode 100644 index 00000000000..549e4f8c65a --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql @@ -0,0 +1,16 @@ +#import './widget.fragment.graphql' + +query WorkItem($id: ID!) { + workItem(id: $id) @client { + id + type + widgets { + nodes { + ...WidgetBase + ... on TitleWidget { + contentText + } + } + } + } +} diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index a635d43776d..7cc8a23b7b1 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -1,11 +1,15 @@ import Vue from 'vue'; import App from './components/app.vue'; +import { createRouter } from './router'; +import { createApolloProvider } from './graphql/provider'; export const initWorkItemsRoot = () => { const el = document.querySelector('#js-work-items'); return new Vue({ el, + router: createRouter(el.dataset.fullPath), + apolloProvider: createApolloProvider(), render(createElement) { return createElement(App); }, diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue new file mode 100644 index 00000000000..e2ae15e0c7c --- /dev/null +++ b/app/assets/javascripts/work_items/pages/work_item_root.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js new file mode 100644 index 00000000000..142fab8cfa6 --- /dev/null +++ b/app/assets/javascripts/work_items/router/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { routes } from './routes'; + +Vue.use(VueRouter); + +export function createRouter(fullPath) { + return new VueRouter({ + routes, + mode: 'history', + base: joinPaths(fullPath, '-', 'work_items'), + }); +} diff --git a/app/assets/javascripts/work_items/router/routes.js b/app/assets/javascripts/work_items/router/routes.js new file mode 100644 index 00000000000..a3cf44ad4ca --- /dev/null +++ b/app/assets/javascripts/work_items/router/routes.js @@ -0,0 +1,8 @@ +export const routes = [ + { + path: '/:id', + name: 'work_item', + component: () => import('../pages/work_item_root.vue'), + props: true, + }, +]; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b22167a3952..3af1afab06e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -163,7 +163,8 @@ class ApplicationController < ActionController::Base payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id payload[:metadata] = @current_context - + payload[:request_urgency] = urgency&.name + payload[:target_duration_s] = urgency&.duration logged_user = auth_user if logged_user.present? payload[:user_id] = logged_user.try(:id) diff --git a/app/controllers/concerns/workhorse_authorization.rb b/app/controllers/concerns/workhorse_authorization.rb index a290ba256b6..648e6f409e6 100644 --- a/app/controllers/concerns/workhorse_authorization.rb +++ b/app/controllers/concerns/workhorse_authorization.rb @@ -38,6 +38,6 @@ module WorkhorseAuthorization end def file_extension_whitelist - ImportExportUploader::EXTENSION_WHITELIST + ImportExportUploader::EXTENSION_ALLOWLIST end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0041ec5135c..ef25ced8610 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -780,6 +780,10 @@ module Ci strong_memoize(:legacy_trigger) { trigger_requests.first } end + def variables_builder + @variables_builder ||= ::Gitlab::Ci::Variables::Builder.new(self) + end + def persisted_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| break variables unless persisted? @@ -1254,6 +1258,12 @@ module Ci self.builds.latest.build_matchers(project) end + def predefined_vars_in_builder_enabled? + strong_memoize(:predefined_vars_in_builder_enabled) do + Feature.enabled?(:ci_predefined_vars_in_builder, project, default_enabled: :yaml) + end + end + private def add_message(severity, content) diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index 27a704c1de0..6871482f71d 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -10,8 +10,10 @@ module Ci # Variables in the environment name scope. # def scoped_variables(environment: expanded_environment_name, dependencies: true) - Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.concat(predefined_variables) + track_duration do + variables = pipeline.variables_builder.scoped_variables(self, environment: environment, dependencies: dependencies) + + variables.concat(predefined_variables) unless pipeline.predefined_vars_in_builder_enabled? variables.concat(project.predefined_variables) variables.concat(pipeline.predefined_variables) variables.concat(runner.predefined_variables) if runnable? && runner @@ -25,9 +27,23 @@ module Ci variables.concat(trigger_request.user_variables) if trigger_request variables.concat(pipeline.variables) variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule + + variables end end + def track_duration + start_time = ::Gitlab::Metrics::System.monotonic_time + result = yield + duration = ::Gitlab::Metrics::System.monotonic_time - start_time + + ::Gitlab::Ci::Pipeline::Metrics + .pipeline_builder_scoped_variables_histogram + .observe({}, duration.seconds) + + result + end + ## # Variables that do not depend on the environment name. # diff --git a/app/uploaders/bulk_imports/export_uploader.rb b/app/uploaders/bulk_imports/export_uploader.rb index 356e5ce028e..cd6e599054b 100644 --- a/app/uploaders/bulk_imports/export_uploader.rb +++ b/app/uploaders/bulk_imports/export_uploader.rb @@ -2,6 +2,6 @@ module BulkImports class ExportUploader < ImportExportUploader - EXTENSION_WHITELIST = %w[ndjson.gz].freeze + EXTENSION_ALLOWLIST = %w[ndjson.gz].freeze end end diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb index 369afba2bae..7b161d72efb 100644 --- a/app/uploaders/import_export_uploader.rb +++ b/app/uploaders/import_export_uploader.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true class ImportExportUploader < AttachmentUploader - EXTENSION_WHITELIST = %w[tar.gz gz].freeze + EXTENSION_ALLOWLIST = %w[tar.gz gz].freeze def self.workhorse_local_upload_path File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH) end def extension_whitelist - EXTENSION_WHITELIST + EXTENSION_ALLOWLIST end def move_to_cache diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml index 052db598571..0efd7a740d3 100644 --- a/app/views/projects/work_items/index.html.haml +++ b/app/views/projects/work_items/index.html.haml @@ -1,3 +1,3 @@ - page_title s_('WorkItem|Work Items') -#js-work-items +#js-work-items{ data: { full_path: @project.full_path } } diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index f48bfcd0e72..760fe18ddec 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -10,6 +10,7 @@ = visibility_level_label(level) .option-description = visibility_level_description(level, form_model) + .option-disabled-reason .text-muted - if all_visibility_levels_restricted? diff --git a/config/feature_flags/development/ci_predefined_vars_in_builder.yml b/config/feature_flags/development/ci_predefined_vars_in_builder.yml new file mode 100644 index 00000000000..5aacf6ee681 --- /dev/null +++ b/config/feature_flags/development/ci_predefined_vars_in_builder.yml @@ -0,0 +1,8 @@ +--- +name: ci_predefined_vars_in_builder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72348 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/231300 +milestone: '14.4' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/initializers/0_marginalia.rb b/config/initializers/0_marginalia.rb index f7a1f5f0469..805a9e33347 100644 --- a/config/initializers/0_marginalia.rb +++ b/config/initializers/0_marginalia.rb @@ -19,7 +19,10 @@ Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint # adding :line has some overhead because a regexp on the backtrace has # to be run on every SQL query. Only enable this in development because # we've seen it slow things down. -Marginalia::Comment.components << :line if Rails.env.development? +if Rails.env.development? + Marginalia::Comment.components << :line + Marginalia::Comment.lines_to_ignore = Regexp.union(Gitlab::BacktraceCleaner::IGNORE_BACKTRACES + %w(lib/ruby/gems/ lib/gem_extensions/ lib/ruby/)) +end Gitlab::Marginalia.set_application_name diff --git a/config/routes/project.rb b/config/routes/project.rb index b1be9ad2ada..496d63785e6 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -358,7 +358,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get 'details', on: :member end - resources :work_items, only: [:index] + get 'work_items/*work_items_path' => 'work_items#index', as: :work_items resource :tracing, only: [:show] diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 934ec8d6a83..78e3314b189 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -41,6 +41,7 @@ The following metrics are available: | `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | `controller`, `action` | | `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | | | `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller or action | `controller`, `action`, `operation` | +| `gitlab_ci_pipeline_builder_scoped_variables_duration` | Histogram | 14.5 | Time in seconds it takes to create the scoped variables for a CI/CD job | `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | | | `gitlab_ci_pipeline_size_builds` | Histogram | 13.1 | Total number of builds within a pipeline grouped by a pipeline source | `source` | | `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 277a34ea667..dc2d53a0e05 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4042,6 +4042,7 @@ Input type: `ScanExecutionPolicyCommitInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `name` | [`String`](#string) | Name of the policy. If the name is null, the `name` field from `policy_yaml` is used. | | `operationMode` | [`MutationOperationMode!`](#mutationoperationmode) | Changes the operation mode. | | `policyYaml` | [`String!`](#string) | YAML snippet of the policy. | | `projectPath` | [`ID!`](#id) | Full path of the project. | diff --git a/doc/api/integrations.md b/doc/api/integrations.md index 3c649e8d044..64154971d03 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -94,9 +94,9 @@ Parameters: | `restrict_to_branch` | string | false | Comma-separated list of branches to be are automatically inspected. Leave blank to include all branches. | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Asana integration +### Disable Asana integration -Delete Asana integration for a project. +Disable the Asana integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/asana @@ -130,9 +130,9 @@ Parameters: | `subdomain` | string | false | The subdomain setting | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Assembla integration +### Disable Assembla integration -Delete Assembla integration for a project. +Disable the Assembla integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/assembla @@ -170,9 +170,9 @@ Parameters: | `password` | string | true | Password of the user | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Atlassian Bamboo CI integration +### Disable Atlassian Bamboo CI integration -Delete Atlassian Bamboo CI integration for a project. +Disable the Atlassian Bamboo CI integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/bamboo @@ -209,9 +209,9 @@ Parameters: | `title` | string | false | Title | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Bugzilla integration +### Disable Bugzilla integration -Delete Bugzilla integration for a project. +Disable the Bugzilla integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/bugzilla @@ -246,9 +246,9 @@ Parameters: | `enable_ssl_verification` | boolean | false | DEPRECATED: This parameter has no effect since SSL verification is always enabled | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Buildkite integration +### Disable Buildkite integration -Delete Buildkite integration for a project. +Disable the Buildkite integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/buildkite @@ -284,9 +284,9 @@ Parameters: | `room` | string | false | Campfire room. The last part of the URL when you're in a room. | | `push_events` | boolean | false | Enable notifications for push events. | -### Delete Campfire integration +### Disable Campfire integration -Delete Campfire integration for a project. +Disable the Campfire integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/campfire @@ -322,9 +322,9 @@ Parameters: | `datadog_service` | string | false | Name of this GitLab instance that all data will be tagged with | | `datadog_env` | string | false | The environment tag that traces will be tagged with | -### Delete Datadog integration +### Disable Datadog integration -Delete Datadog integration for a project. +Disable the Datadog integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/datadog @@ -367,9 +367,9 @@ Parameters: | `pipeline_events` | boolean | false | Enable notifications for pipeline events | | `wiki_page_events` | boolean | false | Enable notifications for wiki page events | -### Delete Unify Circuit integration +### Disable Unify Circuit integration -Delete Unify Circuit integration for a project. +Disable the Unify Circuit integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/unify-circuit @@ -412,9 +412,9 @@ Parameters: | `pipeline_events` | boolean | false | Enable notifications for pipeline events | | `wiki_page_events` | boolean | false | Enable notifications for wiki page events | -### Delete Webex Teams integration +### Disable Webex Teams integration -Delete Webex Teams integration for a project. +Disable the Webex Teams integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/webex-teams @@ -451,9 +451,9 @@ Parameters: | `title` | string | false | Title | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Custom Issue Tracker integration +### Disable Custom Issue Tracker integration -Delete Custom Issue Tracker integration for a project. +Disable the Custom Issue Tracker integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/custom-issue-tracker @@ -485,9 +485,9 @@ Parameters: | --------- | ---- | -------- | ----------- | | `webhook` | string | true | Discord webhook. For example, `https://discord.com/api/webhooks/…` | -### Delete Discord integration +### Disable Discord integration -Delete Discord integration for a project. +Disable the Discord integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/discord @@ -524,9 +524,9 @@ Parameters: | `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `tag_push_events` | boolean | false | Enable notifications for tag push events | -### Delete Drone CI integration +### Disable Drone CI integration -Delete Drone CI integration for a project. +Disable the Drone CI integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/drone-ci @@ -563,9 +563,9 @@ Parameters: | `tag_push_events` | boolean | false | Enable notifications for tag push events | | `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected". Notifications are always fired for tag pushes. The default value is "all" | -### Delete Emails on Push integration +### Disable Emails on Push integration -Delete Emails on Push integration for a project. +Disable the Emails on Push integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/emails-on-push @@ -599,9 +599,9 @@ Parameters: | `project_url` | string | true | The URL to the project in EWM | | `issues_url` | string | true | The URL to view an issue in EWM. Must contain `:id` | -### Delete EWM integration +### Disable EWM integration -Delete EWM integration for a project. +Disable the EWM integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/ewm @@ -635,9 +635,9 @@ Parameters: | --------- | ---- | -------- | ----------- | | `confluence_url` | string | true | The URL of the Confluence Cloud Workspace hosted on atlassian.net. | -### Delete Confluence integration +### Disable Confluence integration -Delete Confluence integration for a project. +Disable the Confluence integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/confluence @@ -669,9 +669,9 @@ Parameters: | --------- | ---- | -------- | ----------- | | `external_wiki_url` | string | true | The URL of the external wiki | -### Delete External wiki integration +### Disable External wiki integration -Delete External wiki integration for a project. +Disable the External wiki integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/external-wiki @@ -706,9 +706,9 @@ Parameters: | `token` | string | true | Flowdock Git source token | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Flowdock integration +### Disable Flowdock integration -Delete Flowdock integration for a project. +Disable the Flowdock integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/flowdock @@ -742,9 +742,9 @@ Parameters: | `repository_url` | string | true | GitHub repository URL | | `static_context` | boolean | false | Append instance name instead of branch to [status check name](../user/project/integrations/github.md#static--dynamic-status-check-names) | -### Delete GitHub integration +### Disable GitHub integration -Delete GitHub integration for a project. +Disable the GitHub integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/github @@ -788,9 +788,9 @@ Parameters: | `pipeline_events` | boolean | false | Enable notifications for pipeline events | | `wiki_page_events` | boolean | false | Enable notifications for wiki page events | -### Delete Hangouts Chat integration +### Disable Hangouts Chat integration -Delete Hangouts Chat integration for a project. +Disable the Hangouts Chat integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/hangouts-chat @@ -829,9 +829,9 @@ Parameters: | `colorize_messages` | boolean | false | Colorize messages | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Irker (IRC gateway) integration +### Disable Irker (IRC gateway) integration -Delete Irker (IRC gateway) integration for a project. +Disable the Irker (IRC gateway) integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/irker @@ -880,9 +880,9 @@ Parameters: | `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) | -### Delete Jira integration +### Disable Jira integration -Remove all previously Jira integrations from a project. +Disable the Jira integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/jira @@ -939,9 +939,9 @@ Parameters: | --------- | ---- | -------- | ----------- | | `token` | string | yes | The Slack token | -### Delete Slack Slash Command integration +### Disable Slack Slash Command integration -Delete Slack Slash Command integration for a project. +Disable the Slack Slash Command integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/slack-slash-commands @@ -974,9 +974,9 @@ Parameters: | `token` | string | yes | The Mattermost token | | `username` | string | no | The username to use to post the message | -### Delete Mattermost Slash Command integration +### Disable Mattermost Slash Command integration -Delete Mattermost Slash Command integration for a project. +Disable the Mattermost Slash Command integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/mattermost-slash-commands @@ -1005,9 +1005,9 @@ Parameters: | `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `tag_push_events` | boolean | false | Enable notifications for tag push events | -### Delete Packagist integration +### Disable Packagist integration -Delete Packagist integration for a project. +Disable the Packagist integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/packagist @@ -1044,9 +1044,9 @@ Parameters: | `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28271)) | | `pipeline_events` | boolean | false | Enable notifications for pipeline events | -### Delete Pipeline-Emails integration +### Disable Pipeline-Emails integration -Delete Pipeline-Emails integration for a project. +Disable the Pipeline-Emails integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/pipelines-email @@ -1082,9 +1082,9 @@ Parameters: | `restrict_to_branch` | boolean | false | Comma-separated list of branches to automatically inspect. Leave blank to include all branches. | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Pivotal Tracker integration +### Disable Pivotal Tracker integration -Delete Pivotal Tracker integration for a project. +Disable the Pivotal Tracker integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/pivotaltracker @@ -1118,9 +1118,9 @@ Parameters: | `google_iap_audience_client_id` | string | false | Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com) | | `google_iap_service_account_json` | string | false | `credentials.json` file for your service account, like { "type": "service_account", "project_id": ... } | -### Delete Prometheus integration +### Disable Prometheus integration -Delete Prometheus integration for a project. +Disable the Prometheus integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/prometheus @@ -1157,9 +1157,9 @@ Parameters: | `sound` | string | false | The sound of the notification | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Pushover integration +### Disable Pushover integration -Delete Pushover integration for a project. +Disable the Pushover integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/pushover @@ -1195,9 +1195,9 @@ Parameters: | `description` | string | false | Description | | `push_events` | boolean | false | Enable notifications for push events | -### Delete Redmine integration +### Disable Redmine integration -Delete Redmine integration for a project. +Disable the Redmine integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/redmine @@ -1256,9 +1256,9 @@ Parameters: | `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications | | `wiki_page_events` | boolean | false | Enable notifications for wiki page events | -### Delete Slack integration +### Disable Slack integration -Delete Slack integration for a project. +Disable the Slack integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/slack @@ -1302,9 +1302,9 @@ Parameters: | `pipeline_events` | boolean | false | Enable notifications for pipeline events | | `wiki_page_events` | boolean | false | Enable notifications for wiki page events | -### Delete Microsoft Teams integration +### Disable Microsoft Teams integration -Delete Microsoft Teams integration for a project. +Disable the Microsoft Teams integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/microsoft-teams @@ -1359,9 +1359,9 @@ Parameters: | `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications | | `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications | -### Delete Mattermost notifications integration +### Disable Mattermost notifications integration -Delete Mattermost notifications integration for a project. +Disable the Mattermost notifications integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/mattermost @@ -1399,9 +1399,9 @@ Parameters: | `password` | string | true | The password of the user | | `push_events` | boolean | false | Enable notifications for push events | -### Delete JetBrains TeamCity CI integration +### Disable JetBrains TeamCity CI integration -Delete JetBrains TeamCity CI integration for a project. +Disable the JetBrains TeamCity CI integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/teamcity @@ -1439,9 +1439,9 @@ Parameters: | `merge_requests_events` | boolean | false | Enable notifications for merge request events. | | `tag_push_events` | boolean | false | Enable notifications for tag push events. | -### Delete Jenkins CI integration +### Disable Jenkins CI integration -Delete Jenkins CI integration for a project. +Disable the Jenkins CI integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/jenkins @@ -1476,9 +1476,9 @@ Parameters: - `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin - `pass_unstable` (optional) - Unstable builds are treated as passing -### Delete Jenkins CI (Deprecated) integration +### Disable Jenkins CI (Deprecated) integration -Delete Jenkins CI (Deprecated) integration for a project. +Disable the Jenkins CI (Deprecated) integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/jenkins-deprecated @@ -1512,9 +1512,9 @@ Parameters: | --------- | ---- | -------- | ----------- | | `mock_service_url` | string | true | `http://localhost:4004` | -### Delete MockCI integration +### Disable MockCI integration -Delete MockCI integration for a project. +Disable the MockCI integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/mock-ci @@ -1549,9 +1549,9 @@ Parameters: | `description` | string | false | Description | | `push_events` | boolean | false | Enable notifications for push events | -### Delete YouTrack integration +### Disable YouTrack integration -Delete YouTrack integration for a project. +Disable the YouTrack integration for a project. Integration settings are preserved. ```plaintext DELETE /projects/:id/integrations/youtrack diff --git a/lib/api/api.rb b/lib/api/api.rb index a4d42c735cb..0d5cf2792af 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -27,7 +27,8 @@ module API Gitlab::GrapeLogging::Loggers::PerfLogger.new, Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new, Gitlab::GrapeLogging::Loggers::ContextLogger.new, - Gitlab::GrapeLogging::Loggers::ContentLogger.new + Gitlab::GrapeLogging::Loggers::ContentLogger.new, + Gitlab::GrapeLogging::Loggers::UrgencyLogger.new ] allow_access_with_scope :api diff --git a/lib/api/helpers/file_upload_helpers.rb b/lib/api/helpers/file_upload_helpers.rb index dd551ec2976..751972b44f0 100644 --- a/lib/api/helpers/file_upload_helpers.rb +++ b/lib/api/helpers/file_upload_helpers.rb @@ -5,7 +5,7 @@ module API module FileUploadHelpers def file_is_valid? filename = params[:file]&.original_filename - filename && ImportExportUploader::EXTENSION_WHITELIST.include?(File.extname(filename).delete('.')) + filename && ImportExportUploader::EXTENSION_ALLOWLIST.include?(File.extname(filename).delete('.')) end def validate_file! diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb index 321efa7854f..b5e48f210ad 100644 --- a/lib/gitlab/ci/pipeline/metrics.rb +++ b/lib/gitlab/ci/pipeline/metrics.rb @@ -51,6 +51,15 @@ module Gitlab ::Gitlab::Metrics.histogram(name, comment, labels, buckets) end + def self.pipeline_builder_scoped_variables_histogram + name = :gitlab_ci_pipeline_builder_scoped_variables_duration + comment = 'Pipeline variables builder scoped_variables duration' + labels = {} + buckets = [0.01, 0.05, 0.1, 0.3, 0.5, 1, 2, 5, 10, 30, 60, 120] + + ::Gitlab::Metrics.histogram(name, comment, labels, buckets) + end + def self.pipeline_processing_events_counter name = :gitlab_ci_pipeline_processing_events_total comment = 'Total amount of pipeline processing events' diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb new file mode 100644 index 00000000000..f4c5a06af97 --- /dev/null +++ b/lib/gitlab/ci/variables/builder.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Variables + class Builder + include ::Gitlab::Utils::StrongMemoize + + def initialize(pipeline) + @pipeline = pipeline + end + + def scoped_variables(job, environment:, dependencies:) + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.concat(predefined_variables(job)) if pipeline.predefined_vars_in_builder_enabled? + end + end + + private + + attr_reader :pipeline + + def predefined_variables(job) + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_JOB_NAME', value: job.name) + variables.append(key: 'CI_JOB_STAGE', value: job.stage) + variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action? + variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request + + variables.append(key: 'CI_NODE_INDEX', value: job.options[:instance].to_s) if job.options&.include?(:instance) + variables.append(key: 'CI_NODE_TOTAL', value: ci_node_total_value(job).to_s) + + # legacy variables + variables.append(key: 'CI_BUILD_NAME', value: job.name) + variables.append(key: 'CI_BUILD_STAGE', value: job.stage) + variables.append(key: 'CI_BUILD_TRIGGERED', value: 'true') if job.trigger_request + variables.append(key: 'CI_BUILD_MANUAL', value: 'true') if job.action? + end + end + + def ci_node_total_value(job) + parallel = job.options&.dig(:parallel) + parallel = parallel.dig(:total) if parallel.is_a?(Hash) + parallel || 1 + end + end + end + end +end diff --git a/lib/gitlab/grape_logging/loggers/urgency_logger.rb b/lib/gitlab/grape_logging/loggers/urgency_logger.rb new file mode 100644 index 00000000000..0a503086d05 --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/urgency_logger.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module GrapeLogging + module Loggers + class UrgencyLogger < ::GrapeLogging::Loggers::Base + def parameters(request, _) + endpoint = request.env['api.endpoint'] + return {} unless endpoint + + urgency = endpoint.options[:for].try(:urgency_for_app, endpoint) + return {} unless urgency + + { request_urgency: urgency.name, target_duration_s: urgency.duration } + end + end + end + end +end diff --git a/lib/gitlab/health_checks/redis/cache_check.rb b/lib/gitlab/health_checks/redis/cache_check.rb index 0c8fe83893b..bd843bdaac4 100644 --- a/lib/gitlab/health_checks/redis/cache_check.rb +++ b/lib/gitlab/health_checks/redis/cache_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class CacheCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_cache_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::Cache.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/health_checks/redis/queues_check.rb b/lib/gitlab/health_checks/redis/queues_check.rb index b1e33b9f459..fb92db937dc 100644 --- a/lib/gitlab/health_checks/redis/queues_check.rb +++ b/lib/gitlab/health_checks/redis/queues_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class QueuesCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_queues_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::Queues.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/health_checks/redis/rate_limiting_check.rb b/lib/gitlab/health_checks/redis/rate_limiting_check.rb index 67c14e26361..0e9d94f7dff 100644 --- a/lib/gitlab/health_checks/redis/rate_limiting_check.rb +++ b/lib/gitlab/health_checks/redis/rate_limiting_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class RateLimitingCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_rate_limiting_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::RateLimiting.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/health_checks/redis/redis_abstract_check.rb b/lib/gitlab/health_checks/redis/redis_abstract_check.rb new file mode 100644 index 00000000000..ecad4b06ea9 --- /dev/null +++ b/lib/gitlab/health_checks/redis/redis_abstract_check.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module HealthChecks + module Redis + module RedisAbstractCheck + include SimpleAbstractCheck + + def check_up + successful?(check) + end + + private + + def redis_instance_class_name + Gitlab::Redis.const_get(redis_instance_name.camelize, false) + end + + def metric_prefix + "redis_#{redis_instance_name}_ping" + end + + def redis_instance_name + name.sub(/_check$/, '') + end + + def successful?(result) + result == 'PONG' + end + + # rubocop: disable CodeReuse/ActiveRecord + def check + catch_timeout 10.seconds do + redis_instance_class_name.with(&:ping) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/gitlab/health_checks/redis/redis_check.rb b/lib/gitlab/health_checks/redis/redis_check.rb index 25879c18f84..c793a939abd 100644 --- a/lib/gitlab/health_checks/redis/redis_check.rb +++ b/lib/gitlab/health_checks/redis/redis_check.rb @@ -14,16 +14,22 @@ module Gitlab end def successful?(result) - result == 'PONG' + result == true end def check - ::Gitlab::HealthChecks::Redis::CacheCheck.check_up && - ::Gitlab::HealthChecks::Redis::QueuesCheck.check_up && - ::Gitlab::HealthChecks::Redis::SharedStateCheck.check_up && - ::Gitlab::HealthChecks::Redis::TraceChunksCheck.check_up && - ::Gitlab::HealthChecks::Redis::RateLimitingCheck.check_up && - ::Gitlab::HealthChecks::Redis::SessionsCheck.check_up + redis_health_checks.all?(&:check_up) + end + + def redis_health_checks + [ + Gitlab::HealthChecks::Redis::CacheCheck, + Gitlab::HealthChecks::Redis::QueuesCheck, + Gitlab::HealthChecks::Redis::SharedStateCheck, + Gitlab::HealthChecks::Redis::TraceChunksCheck, + Gitlab::HealthChecks::Redis::RateLimitingCheck, + Gitlab::HealthChecks::Redis::SessionsCheck + ] end end end diff --git a/lib/gitlab/health_checks/redis/sessions_check.rb b/lib/gitlab/health_checks/redis/sessions_check.rb index a0c5e177b4e..90a4c868f40 100644 --- a/lib/gitlab/health_checks/redis/sessions_check.rb +++ b/lib/gitlab/health_checks/redis/sessions_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class SessionsCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_sessions_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::Sessions.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/health_checks/redis/shared_state_check.rb b/lib/gitlab/health_checks/redis/shared_state_check.rb index 285ac271929..80f91784b8c 100644 --- a/lib/gitlab/health_checks/redis/shared_state_check.rb +++ b/lib/gitlab/health_checks/redis/shared_state_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class SharedStateCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_shared_state_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::SharedState.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/health_checks/redis/trace_chunks_check.rb b/lib/gitlab/health_checks/redis/trace_chunks_check.rb index cf9fa700b0a..9a89a1ce51d 100644 --- a/lib/gitlab/health_checks/redis/trace_chunks_check.rb +++ b/lib/gitlab/health_checks/redis/trace_chunks_check.rb @@ -4,31 +4,7 @@ module Gitlab module HealthChecks module Redis class TraceChunksCheck - extend SimpleAbstractCheck - - class << self - def check_up - check - end - - private - - def metric_prefix - 'redis_trace_chunks_ping' - end - - def successful?(result) - result == 'PONG' - end - - # rubocop: disable CodeReuse/ActiveRecord - def check - catch_timeout 10.seconds do - Gitlab::Redis::TraceChunks.with(&:ping) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end + extend RedisAbstractCheck end end end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index fdc4c22001f..6d3b92afb53 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -56,10 +56,20 @@ module Gitlab end def download(url, upload_path) - File.open(upload_path, 'w') do |file| - # Download (stream) file from the uploader's location - IO.copy_stream(URI.parse(url).open, file) + File.open(upload_path, 'wb') do |file| + Gitlab::HTTP.get(url, stream_body: true) do |fragment| + if [301, 302, 307].include?(fragment.code) + Gitlab::Import::Logger.warn(message: "received redirect fragment", fragment_code: fragment.code) + elsif fragment.code == 200 + file.write(fragment) + else + raise Gitlab::ImportExport::Error, "unsupported response downloading fragment #{fragment.code}" + end + end end + rescue StandardError => e + @shared.error(e) # rubocop:disable Gitlab/ModuleWithInstanceVariables + raise e end def tar_with_options(archive:, dir:, options:) diff --git a/lib/gitlab/lograge/custom_options.rb b/lib/gitlab/lograge/custom_options.rb index 83fd74310d0..e6c9ba0773c 100644 --- a/lib/gitlab/lograge/custom_options.rb +++ b/lib/gitlab/lograge/custom_options.rb @@ -7,6 +7,8 @@ module Gitlab LIMITED_ARRAY_SENTINEL = { key: 'truncated', value: '...' }.freeze IGNORE_PARAMS = Set.new(%w(controller action format)).freeze + KNOWN_PAYLOAD_PARAMS = [:remote_ip, :user_id, :username, :ua, :queue_duration_s, + :etag_route, :request_urgency, :target_duration_s] + CLOUDFLARE_CUSTOM_HEADERS.values def self.call(event) params = event @@ -14,24 +16,17 @@ module Gitlab .each_with_object([]) { |(k, v), array| array << { key: k, value: v } unless IGNORE_PARAMS.include?(k) } payload = { time: Time.now.utc.iso8601(3), - params: Gitlab::Utils::LogLimitedArray.log_limited_array(params, sentinel: LIMITED_ARRAY_SENTINEL), - remote_ip: event.payload[:remote_ip], - user_id: event.payload[:user_id], - username: event.payload[:username], - ua: event.payload[:ua] + params: Gitlab::Utils::LogLimitedArray.log_limited_array(params, sentinel: LIMITED_ARRAY_SENTINEL) } + payload.merge!(event.payload[:metadata]) if event.payload[:metadata] + optional_payload_params = event.payload.slice(*KNOWN_PAYLOAD_PARAMS).compact + payload.merge!(optional_payload_params) ::Gitlab::InstrumentationHelper.add_instrumentation_data(payload) - payload[:queue_duration_s] = event.payload[:queue_duration_s] if event.payload[:queue_duration_s] - payload[:etag_route] = event.payload[:etag_route] if event.payload[:etag_route] payload[Labkit::Correlation::CorrelationId::LOG_KEY] = event.payload[Labkit::Correlation::CorrelationId::LOG_KEY] || Labkit::Correlation::CorrelationId.current_id - CLOUDFLARE_CUSTOM_HEADERS.each do |_, value| - payload[value] = event.payload[value] if event.payload[value] - end - # https://github.com/roidrage/lograge#logging-errors--exceptions exception = event.payload[:exception_object] diff --git a/lib/gitlab/metrics/rails_slis.rb b/lib/gitlab/metrics/rails_slis.rb index 69e0c1e9fde..a8bf216e452 100644 --- a/lib/gitlab/metrics/rails_slis.rb +++ b/lib/gitlab/metrics/rails_slis.rb @@ -30,10 +30,12 @@ module Gitlab endpoint_id = API::Base.endpoint_id_for_route(route) route_class = route.app.options[:for] feature_category = route_class.feature_category_for_app(route.app) + request_urgency = route_class.urgency_for_app(route.app) { endpoint_id: endpoint_id, - feature_category: feature_category + feature_category: feature_category, + request_urgency: request_urgency.name } end end @@ -42,7 +44,8 @@ module Gitlab Gitlab::RequestEndpoints.all_controller_actions.map do |controller, action| { endpoint_id: controller.endpoint_id_for_action(action), - feature_category: controller.feature_category_for_action(action) + feature_category: controller.feature_category_for_action(action), + request_urgency: controller.urgency_for_action(action).name } end end diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb index 3a0e34d5615..c976023c05a 100644 --- a/lib/gitlab/metrics/requests_rack_middleware.rb +++ b/lib/gitlab/metrics/requests_rack_middleware.rb @@ -116,9 +116,11 @@ module Gitlab def record_apdex_if_needed(env, elapsed) return unless Gitlab::Metrics::RailsSlis.request_apdex_counters_enabled? + urgency = urgency_for_env(env) + Gitlab::Metrics::RailsSlis.request_apdex.increment( - labels: labels_from_context, - success: satisfactory?(env, elapsed) + labels: labels_from_context.merge(request_urgency: urgency.name), + success: elapsed < urgency.duration ) end @@ -129,17 +131,15 @@ module Gitlab } end - def satisfactory?(env, elapsed) - target = + def urgency_for_env(env) + endpoint_urgency = if env['api.endpoint'].present? env['api.endpoint'].options[:for].try(:urgency_for_app, env['api.endpoint']) elsif env['action_controller.instance'].present? && env['action_controller.instance'].respond_to?(:urgency) env['action_controller.instance'].urgency end - target ||= Gitlab::EndpointAttributes::DEFAULT_URGENCY - - elapsed < target.duration + endpoint_urgency || Gitlab::EndpointAttributes::DEFAULT_URGENCY end end end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 560ca7da1a2..26a2a668cc1 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -81,7 +81,7 @@ module QA result = yield.tap do fabrication_time = Time.now - start - Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time * 1000) + Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time) Runtime::Logger.debug do msg = ["==#{'=' * parents.size}>"] msg << "Built a #{name}" diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 3f6a4eee5ac..26c800d470a 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -27,7 +27,9 @@ module QA :import_error attribute :group do - Group.fabricate! + Group.fabricate! do |group| + group.api_client = api_client + end end attribute :path_with_namespace do diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb index 3f72dd613e5..25c8683971b 100644 --- a/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb @@ -104,7 +104,10 @@ module QA source_issue # fabricate source group, project, issue end - it 'successfully imports issue' do + it( + 'successfully imports issue', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2325' + ) do expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) aggregate_failures do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index e9a49319f21..c4f93de5c23 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -501,11 +501,16 @@ RSpec.describe ApplicationController do describe '#append_info_to_payload' do controller(described_class) do attr_reader :last_payload + urgency :high, [:foo] def index render html: 'authenticated' end + def foo + render html: '' + end + def append_info_to_payload(payload) super @@ -513,6 +518,13 @@ RSpec.describe ApplicationController do end end + before do + routes.draw do + get 'index' => 'anonymous#index' + get 'foo' => 'anonymous#foo' + end + end + it 'does not log errors with a 200 response' do get :index @@ -534,6 +546,22 @@ RSpec.describe ApplicationController do expect(controller.last_payload[:metadata]).to include('meta.user' => user.username) end + + context 'urgency information' do + it 'adds default urgency information to the payload' do + get :index + + expect(controller.last_payload[:request_urgency]).to eq(:default) + expect(controller.last_payload[:target_duration_s]).to eq(1) + end + + it 'adds customized urgency information to the payload' do + get :foo + + expect(controller.last_payload[:request_urgency]).to eq(:high) + expect(controller.last_payload[:target_duration_s]).to eq(0.25) + end + end end describe '#access_denied' do diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js index 8878891701f..9f07eea433a 100644 --- a/spec/frontend/create_merge_request_dropdown_spec.js +++ b/spec/frontend/create_merge_request_dropdown_spec.js @@ -46,7 +46,10 @@ describe('CreateMergeRequestDropdown', () => { dropdown .getRef('contains#hash') .then(() => { - expect(axios.get).toHaveBeenCalledWith(endpoint); + expect(axios.get).toHaveBeenCalledWith( + endpoint, + expect.objectContaining({ cancelToken: expect.anything() }), + ); }) .then(done) .catch(done.fail); diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 0527c2153f4..9b63f84e617 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -702,23 +702,4 @@ describe('diffs/components/app', () => { ); }); }); - - describe('fluid layout', () => { - beforeEach(() => { - setFixtures( - '
', - ); - }); - - it('removes limited container classes when on diffs tab', () => { - createComponent({ isFluidLayout: false, shouldShow: true }, () => {}, { - glFeatures: { mrChangesFluidLayout: true }, - }); - - const containerClassList = document.querySelector('.merge-request-container').classList; - - expect(containerClassList).not.toContain('container-limited'); - expect(containerClassList).not.toContain('limit-container-width'); - }); - }); }); diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js index aa16b71172b..b3f177a1f12 100644 --- a/spec/frontend/projects/new/components/new_project_url_select_spec.js +++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js @@ -24,14 +24,23 @@ describe('NewProjectUrlSelect component', () => { { id: 'gid://gitlab/Group/26', fullPath: 'flightjs', + name: 'Flight JS', + visibility: 'public', + webUrl: 'http://127.0.0.1:3000/flightjs', }, { id: 'gid://gitlab/Group/28', fullPath: 'h5bp', + name: 'H5BP', + visibility: 'public', + webUrl: 'http://127.0.0.1:3000/h5bp', }, { id: 'gid://gitlab/Group/30', fullPath: 'h5bp/subgroup', + name: 'H5BP Subgroup', + visibility: 'private', + webUrl: 'http://127.0.0.1:3000/h5bp/subgroup', }, ], }, @@ -79,6 +88,10 @@ describe('NewProjectUrlSelect component', () => { const findDropdown = () => wrapper.findComponent(GlDropdown); const findInput = () => wrapper.findComponent(GlSearchBoxByType); const findHiddenInput = () => wrapper.find('input'); + const clickDropdownItem = async () => { + wrapper.findComponent(GlDropdownItem).vm.$emit('click'); + await wrapper.vm.$nextTick(); + }; afterEach(() => { wrapper.destroy(); @@ -127,7 +140,6 @@ describe('NewProjectUrlSelect component', () => { it('focuses on the input when the dropdown is opened', async () => { wrapper = mountComponent({ mountFn: mount }); - jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); @@ -140,7 +152,6 @@ describe('NewProjectUrlSelect component', () => { it('renders expected dropdown items', async () => { wrapper = mountComponent({ mountFn: mount }); - jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); @@ -160,7 +171,6 @@ describe('NewProjectUrlSelect component', () => { beforeEach(async () => { wrapper = mountComponent({ mountFn: mount }); - jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); @@ -195,23 +205,38 @@ describe('NewProjectUrlSelect component', () => { }; wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount }); - jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); expect(wrapper.find('li').text()).toBe('No matches found'); }); - it('updates hidden input with selected namespace', async () => { + it('emits `update-visibility` event to update the visibility radio options', async () => { wrapper = mountComponent(); - jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); - wrapper.findComponent(GlDropdownItem).vm.$emit('click'); + const spy = jest.spyOn(eventHub, '$emit'); + await clickDropdownItem(); + + const namespace = data.currentUser.groups.nodes[0]; + + expect(spy).toHaveBeenCalledWith('update-visibility', { + name: namespace.name, + visibility: namespace.visibility, + showPath: namespace.webUrl, + editPath: `${namespace.webUrl}/-/edit`, + }); + }); + + it('updates hidden input with selected namespace', async () => { + wrapper = mountComponent(); + jest.runOnlyPendingTimers(); await wrapper.vm.$nextTick(); + await clickDropdownItem(); + expect(findHiddenInput().attributes()).toMatchObject({ name: 'project[namespace_id]', value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(), diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js index 8243e2bd389..d9b7cd5afa2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js @@ -159,9 +159,8 @@ describe('LabelsSelect Mutations', () => { labels = [ { id: 1, title: 'scoped' }, { id: 2, title: 'scoped::one', set: false }, - { id: 3, title: 'scoped::two', set: false }, - { id: 4, title: 'scoped::three', set: true }, - { id: 5, title: '' }, + { id: 3, title: 'scoped::test', set: true }, + { id: 4, title: '' }, ]; }); @@ -192,9 +191,8 @@ describe('LabelsSelect Mutations', () => { expect(state.labels).toEqual([ { id: 1, title: 'scoped' }, { id: 2, title: 'scoped::one', set: true, touched: true }, - { id: 3, title: 'scoped::two', set: false }, - { id: 4, title: 'scoped::three', set: false }, - { id: 5, title: '' }, + { id: 3, title: 'scoped::test', set: false }, + { id: 4, title: '' }, ]); }); }); diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js new file mode 100644 index 00000000000..95034085493 --- /dev/null +++ b/spec/frontend/work_items/components/app_spec.js @@ -0,0 +1,24 @@ +import { shallowMount } from '@vue/test-utils'; +import App from '~/work_items/components/app.vue'; + +describe('Work Items Application', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(App, { + stubs: { + 'router-view': true, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a component', () => { + createComponent(); + + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js new file mode 100644 index 00000000000..efb4aa2feb2 --- /dev/null +++ b/spec/frontend/work_items/mock_data.js @@ -0,0 +1,17 @@ +export const workItemQueryResponse = { + workItem: { + __typename: 'WorkItem', + id: '1', + type: 'FEATURE', + widgets: { + __typename: 'WorkItemWidgetConnection', + nodes: [ + { + __typename: 'TitleWidget', + type: 'TITLE', + contentText: 'Test', + }, + ], + }, + }, +}; diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js new file mode 100644 index 00000000000..64d02baed36 --- /dev/null +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -0,0 +1,70 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; +import { workItemQueryResponse } from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const WORK_ITEM_ID = '1'; + +describe('Work items root component', () => { + let wrapper; + let fakeApollo; + + const findTitle = () => wrapper.find('[data-testid="title"]'); + + const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => { + fakeApollo = createMockApollo(); + fakeApollo.clients.defaultClient.cache.writeQuery({ + query: workItemQuery, + variables: { + id: WORK_ITEM_ID, + }, + data: queryResponse, + }); + + wrapper = shallowMount(WorkItemsRoot, { + propsData: { + id: WORK_ITEM_ID, + }, + localVue, + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('renders the title if title is in the widgets list', () => { + createComponent(); + + expect(findTitle().exists()).toBe(true); + expect(findTitle().text()).toBe('Test'); + }); + + it('does not render the title if title is not in the widgets list', () => { + const queryResponse = { + workItem: { + ...workItemQueryResponse.workItem, + widgets: { + __typename: 'WorkItemWidgetConnection', + nodes: [ + { + __typename: 'SomeOtherWidget', + type: 'OTHER', + contentText: 'Test', + }, + ], + }, + }, + }; + createComponent({ queryResponse }); + + expect(findTitle().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js new file mode 100644 index 00000000000..0a57eab753f --- /dev/null +++ b/spec/frontend/work_items/router_spec.js @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils'; +import App from '~/work_items/components/app.vue'; +import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; +import { createRouter } from '~/work_items/router'; + +describe('Work items router', () => { + let wrapper; + + const createComponent = async (routeArg) => { + const router = createRouter('/work_item'); + if (routeArg !== undefined) { + await router.push(routeArg); + } + + wrapper = mount(App, { + router, + }); + }; + + afterEach(() => { + wrapper.destroy(); + window.location.hash = ''; + }); + + it('renders work item on `/1` route', async () => { + await createComponent('/1'); + + expect(wrapper.find(WorkItemsRoot).exists()).toBe(true); + }); +}); diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb new file mode 100644 index 00000000000..10275f33484 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Builder do + let(:builder) { described_class.new(pipeline) } + let(:pipeline) { create(:ci_pipeline) } + let(:job) { create(:ci_build, pipeline: pipeline) } + + describe '#scoped_variables' do + let(:environment) { job.expanded_environment_name } + let(:dependencies) { true } + + subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) } + + it 'returns the expected variables' do + keys = %w[CI_JOB_NAME + CI_JOB_STAGE + CI_NODE_TOTAL + CI_BUILD_NAME + CI_BUILD_STAGE] + + subject.map { |env| env[:key] }.tap do |names| + expect(names).to include(*keys) + end + end + + context 'feature flag disabled' do + before do + stub_feature_flags(ci_predefined_vars_in_builder: false) + end + + it 'returns no variables' do + expect(subject.map { |env| env[:key] }).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb new file mode 100644 index 00000000000..464534f0271 --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GrapeLogging::Loggers::UrgencyLogger do + def endpoint(options, namespace: '') + Struct.new(:options, :namespace).new(options, namespace) + end + + let(:api_class) do + Class.new(API::Base) do + namespace 'testing' do + # rubocop:disable Rails/HttpPositionalArguments + # This is not the get that performs a request, but the one from Grape + get 'test', urgency: :high do + {} + end + # rubocop:enable Rails/HttpPositionalArguments + end + end + end + + describe ".parameters" do + where(:request_env, :expected_parameters) do + [ + [{}, {}], + [{ 'api.endpoint' => endpoint({}) }, {}], + [{ 'api.endpoint' => endpoint({ for: 'something weird' }) }, {}], + [ + { 'api.endpoint' => endpoint({ for: api_class, path: [] }) }, + { request_urgency: :default, target_duration_s: 1 } + ], + [ + { 'api.endpoint' => endpoint({ for: api_class, path: ['test'] }, namespace: '/testing') }, + { request_urgency: :high, target_duration_s: 0.25 } + ] + ] + end + + with_them do + let(:request) { double('request', env: request_env) } + + subject { described_class.new.parameters(request, nil) } + + it { is_expected.to eq(expected_parameters) } + end + end +end diff --git a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb index 43e890a6c4f..145d573b6de 100644 --- a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb @@ -4,5 +4,5 @@ require 'spec_helper' require_relative '../simple_check_shared' RSpec.describe Gitlab::HealthChecks::Redis::RedisCheck do - include_examples 'simple_check', 'redis_ping', 'Redis', 'PONG' + include_examples 'simple_check', 'redis_ping', 'Redis', true end diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb index 59c4e1083ae..0d4c0545ae2 100644 --- a/spec/lib/gitlab/import_export/command_line_util_spec.rb +++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb @@ -17,6 +17,10 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do def initialize @shared = Gitlab::ImportExport::Shared.new(nil) end + + def download(url, upload_path) + super(url, upload_path) + end end.new end @@ -101,4 +105,44 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do end end end + + describe '#download' do + before do + stub_request(:get, loc) + .to_return( + status: 200, + body: content + ) + end + + context 'a non-localhost uri' do + let(:loc) { 'https://gitlab.com' } + let(:content) { File.open('spec/fixtures/rails_sample.tif') } + + it 'gets the contents' do + Tempfile.create("foo") do |f| + subject.download(loc, f.path) + expect(f.read).to eq(File.open('spec/fixtures/rails_sample.tif').read) + end + end + + it 'streams the contents' do + expect(Gitlab::HTTP).to receive(:get).with(loc, hash_including(stream_body: true)) + Tempfile.create("foo") do |f| + subject.download(loc, f.path) + end + end + end + + context 'a localhost uri' do + let(:loc) { 'https://localhost:8081/foo/bar' } + let(:content) { 'foo' } + + it 'throws a blocked url error' do + Tempfile.create("foo") do |f| + expect { subject.download(loc, f.path) }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + end + end + end end diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb index 9daedfc37e4..a4ae39a835a 100644 --- a/spec/lib/gitlab/lograge/custom_options_spec.rb +++ b/spec/lib/gitlab/lograge/custom_options_spec.rb @@ -19,7 +19,13 @@ RSpec.describe Gitlab::Lograge::CustomOptions do user_id: 'test', cf_ray: SecureRandom.hex, cf_request_id: SecureRandom.hex, - metadata: { 'meta.user' => 'jane.doe' } + metadata: { 'meta.user' => 'jane.doe' }, + request_urgency: :default, + target_duration_s: 1, + remote_ip: '192.168.1.2', + ua: 'Nyxt', + queue_duration_s: 0.2, + etag_route: '/etag' } end @@ -66,6 +72,18 @@ RSpec.describe Gitlab::Lograge::CustomOptions do end end + context 'trusted payload' do + it { is_expected.to include(event_payload.slice(*described_class::KNOWN_PAYLOAD_PARAMS)) } + + context 'payload with rejected fields' do + let(:event_payload) { { params: {}, request_urgency: :high, something: 'random', username: nil } } + + it { is_expected.to include({ request_urgency: :high }) } + it { is_expected.not_to include({ something: 'random' }) } + it { is_expected.not_to include({ username: nil }) } + end + end + context 'when correlation_id is overridden' do let(:correlation_id_key) { Labkit::Correlation::CorrelationId::LOG_KEY } diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb index 16fcb9d46a2..4409bc13afe 100644 --- a/spec/lib/gitlab/metrics/rails_slis_spec.rb +++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb @@ -17,11 +17,13 @@ RSpec.describe Gitlab::Metrics::RailsSlis do possible_labels = [ { endpoint_id: "GET /api/:version/version", - feature_category: :not_owned + feature_category: :not_owned, + request_urgency: :default }, { endpoint_id: "ProjectsController#show", - feature_category: :projects + feature_category: :projects, + request_urgency: :default } ] diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 5870f9a8f68..1145bda3570 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -36,7 +36,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do it 'tracks request count and duration' do expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown') expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time) - expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true) + expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment) + .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true) subject.call(env) end @@ -122,7 +123,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'issue_tracking') expect(described_class).not_to receive(:http_health_requests_total) expect(Gitlab::Metrics::RailsSlis.request_apdex) - .to receive(:increment).with(labels: { feature_category: 'issue_tracking', endpoint_id: 'IssuesController#show' }, success: true) + .to receive(:increment).with(labels: { feature_category: 'issue_tracking', endpoint_id: 'IssuesController#show', request_urgency: :default }, success: true) subject.call(env) end @@ -156,7 +157,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do it 'sets the required labels to unknown' do expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown') expect(described_class).not_to receive(:http_health_requests_total) - expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true) + expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment) + .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true) subject.call(env) end @@ -206,7 +208,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do it "captures SLI metrics" do expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'hello_world', endpoint_id: 'GET /projects/:id/archive' }, + labels: { + feature_category: 'hello_world', + endpoint_id: 'GET /projects/:id/archive', + request_urgency: request_urgency_name + }, success: success ) subject.call(env) @@ -235,7 +241,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do it "captures SLI metrics" do expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'hello_world', endpoint_id: 'AnonymousController#index' }, + labels: { + feature_category: 'hello_world', + endpoint_id: 'AnonymousController#index', + request_urgency: request_urgency_name + }, success: success ) subject.call(env) @@ -255,17 +265,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do let(:api_handler) { Class.new(::API::Base) } - it "falls back request's expectation to medium (1 second)" do + it "falls back request's expectation to default (1 second)" do allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: true ) subject.call(env) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: false ) subject.call(env) @@ -281,17 +299,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do { 'action_controller.instance' => controller_instance, 'REQUEST_METHOD' => 'GET' } end - it "falls back request's expectation to medium (1 second)" do + it "falls back request's expectation to default (1 second)" do allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: true ) subject.call(env) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: false ) subject.call(env) @@ -303,17 +329,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do { 'REQUEST_METHOD' => 'GET' } end - it "falls back request's expectation to medium (1 second)" do + it "falls back request's expectation to default (1 second)" do allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: true ) subject.call(env) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101) expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with( - labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, + labels: { + feature_category: 'unknown', + endpoint_id: 'unknown', + request_urgency: :default + }, success: false ) subject.call(env) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2ebf75a1d8a..cbf73cdb8b2 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2759,7 +2759,10 @@ RSpec.describe Ci::Build do let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } } before do - allow(build).to receive(:predefined_variables) { [build_pre_var] } + allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder| + allow(builder).to receive(:predefined_variables) { [build_pre_var] } + end + allow(build).to receive(:yaml_variables) { [build_yaml_var] } allow(build).to receive(:persisted_variables) { [] } allow(build).to receive(:job_jwt_variables) { [job_jwt_var] } @@ -3411,75 +3414,122 @@ RSpec.describe Ci::Build do end describe '#scoped_variables' do - context 'when build has not been persisted yet' do - let(:build) do - described_class.new( - name: 'rspec', - stage: 'test', - ref: 'feature', - project: project, - pipeline: pipeline, - scheduling_type: :stage - ) - end + before do + pipeline.clear_memoization(:predefined_vars_in_builder_enabled) + end - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + it 'records a prometheus metric' do + histogram = double(:histogram) + expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_builder_scoped_variables_histogram) + .and_return(histogram) - it 'does not persist the build' do - expect(build).to be_valid - expect(build).not_to be_persisted + expect(histogram).to receive(:observe) + .with({}, a_kind_of(ActiveSupport::Duration)) - build.scoped_variables + build.scoped_variables + end - expect(build).not_to be_persisted - end - - it 'returns static predefined variables' do - keys = %w[CI_JOB_NAME - CI_COMMIT_SHA - CI_COMMIT_SHORT_SHA - CI_COMMIT_REF_NAME - CI_COMMIT_REF_SLUG - CI_JOB_STAGE] - - variables = build.scoped_variables - - variables.map { |env| env[:key] }.tap do |names| - expect(names).to include(*keys) + shared_examples 'calculates scoped_variables' do + context 'when build has not been persisted yet' do + let(:build) do + described_class.new( + name: 'rspec', + stage: 'test', + ref: 'feature', + project: project, + pipeline: pipeline, + scheduling_type: :stage + ) end - expect(variables) - .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false) + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + + it 'does not persist the build' do + expect(build).to be_valid + expect(build).not_to be_persisted + + build.scoped_variables + + expect(build).not_to be_persisted + end + + it 'returns static predefined variables' do + keys = %w[CI_JOB_NAME + CI_COMMIT_SHA + CI_COMMIT_SHORT_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_JOB_STAGE] + + variables = build.scoped_variables + + variables.map { |env| env[:key] }.tap do |names| + expect(names).to include(*keys) + end + + expect(variables) + .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false) + end + + it 'does not return prohibited variables' do + keys = %w[CI_JOB_ID + CI_JOB_URL + CI_JOB_TOKEN + CI_BUILD_ID + CI_BUILD_TOKEN + CI_REGISTRY_USER + CI_REGISTRY_PASSWORD + CI_REPOSITORY_URL + CI_ENVIRONMENT_URL + CI_DEPLOY_USER + CI_DEPLOY_PASSWORD] + + build.scoped_variables.map { |env| env[:key] }.tap do |names| + expect(names).not_to include(*keys) + end + end end - it 'does not return prohibited variables' do - keys = %w[CI_JOB_ID - CI_JOB_URL - CI_JOB_TOKEN - CI_BUILD_ID - CI_BUILD_TOKEN - CI_REGISTRY_USER - CI_REGISTRY_PASSWORD - CI_REPOSITORY_URL - CI_ENVIRONMENT_URL - CI_DEPLOY_USER - CI_DEPLOY_PASSWORD] + context 'with dependency variables' do + let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) } - build.scoped_variables.map { |env| env[:key] }.tap do |names| - expect(names).not_to include(*keys) + let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) } + + it 'inherits dependent variables' do + expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) end end end - context 'with dependency variables' do - let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) } - let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) } + it_behaves_like 'calculates scoped_variables' - let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) } + it 'delegates to the variable builders' do + expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder| + expect(builder) + .to receive(:scoped_variables).with(build, hash_including(:environment, :dependencies)) + .and_call_original - it 'inherits dependent variables' do - expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) + expect(builder).to receive(:predefined_variables).and_call_original end + + build.scoped_variables + end + + context 'when ci builder feature flag is disabled' do + before do + stub_feature_flags(ci_predefined_vars_in_builder: false) + end + + it 'does not delegate to the variable builders' do + expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder| + expect(builder).not_to receive(:predefined_variables) + end + + build.scoped_variables + end + + it_behaves_like 'calculates scoped_variables' end end