diff options
46 files changed, 1181 insertions, 415 deletions
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 7c6c86f5e78..3857303f2c4 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -22,6 +22,7 @@ MUST be linked for the release bot to know that the associated merge requests sh - [ ] Run `scripts/security-harness` in your local repository to prevent accidentally pushing to any remote besides `gitlab.com/gitlab-org/security`. - [ ] Create a new branch prefixing it with `security-`. - [ ] Create a merge request targeting `master` on `gitlab.com/gitlab-org/security` and use the [Security Release merge request template]. +- [ ] If this includes a breaking change, make sure to include a mention of it for the relevant versions in [`doc/update/index.md`](https://gitlab.com/gitlab-org/security/gitlab/-/blob/master/doc/update/index.md#version-specific-upgrading-instructions) After your merge request has been approved according to our [approval guidelines] and by a team member of the AppSec team, you're ready to prepare the backports @@ -46,7 +47,6 @@ After your merge request has been approved according to our [approval guidelines - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details) -- [ ] If this includes a breaking change, make sure it is mentioned for the relevant versions in [`doc/update/index.md`](https://gitlab.com/gitlab-org/security/gitlab/-/blob/master/doc/update/index.md#version-specific-upgrading-instructions) ## Summary diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index d0e55637190..7279197a244 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -ddcce8f5e7878c997a9863f5c3ed532d7126256b +a966c74ae41b0c749ea0433501cc39dbff96ce3f diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue index d468c82893d..b13f7dae2c2 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue @@ -18,6 +18,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { getCookie, setCookie } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import Tracking from '~/tracking'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { allEnvironments, @@ -59,7 +60,7 @@ export default { GlModal, GlSprintf, }, - mixins: [trackingMixin], + mixins: [glFeatureFlagsMixin(), trackingMixin], inject: [ 'awsLogoSvgPath', 'awsTipCommandsLink', @@ -69,6 +70,7 @@ export default { 'environmentScopeLink', 'isProtectedByDefault', 'maskedEnvironmentVariablesLink', + 'maskableRawRegex', 'maskableRegex', ], props: { @@ -115,7 +117,7 @@ export default { }, computed: { canMask() { - const regex = RegExp(this.maskableRegex); + const regex = RegExp(this.useRawMaskableRegexp ? this.maskableRawRegex : this.maskableRegex); return regex.test(this.variable.value); }, canSubmit() { @@ -128,11 +130,17 @@ export default { displayMaskedError() { return !this.canMask && this.variable.masked; }, + isUsingRawRegexFlag() { + return this.glFeatures.ciRemoveCharacterLimitationRawMaskedVar; + }, isEditing() { return this.mode === EDIT_VARIABLE_ACTION; }, isExpanded() { - return !this.variable.raw; + return !this.isRaw; + }, + isRaw() { + return this.variable.raw; }, isTipVisible() { return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); @@ -168,6 +176,9 @@ export default { return true; }, + useRawMaskableRegexp() { + return this.isRaw && this.isUsingRawRegexFlag; + }, variableValidationFeedback() { return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; }, @@ -322,11 +333,7 @@ export default { class="gl-font-monospace!" spellcheck="false" /> - <p - v-if="variable.raw" - class="gl-mt-2 gl-mb-0 text-secondary" - data-testid="raw-variable-tip" - > + <p v-if="isRaw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip"> {{ __('Variable value will be evaluated as raw string.') }} </p> </gl-form-group> diff --git a/app/assets/javascripts/ci/ci_variable_list/index.js b/app/assets/javascripts/ci/ci_variable_list/index.js index afb390746f9..4270c3c67fc 100644 --- a/app/assets/javascripts/ci/ci_variable_list/index.js +++ b/app/assets/javascripts/ci/ci_variable_list/index.js @@ -21,6 +21,7 @@ const mountCiVariableListApp = (containerEl) => { isGroup, isProject, maskedEnvironmentVariablesLink, + maskableRawRegex, maskableRegex, projectFullPath, projectId, @@ -62,6 +63,7 @@ const mountCiVariableListApp = (containerEl) => { isProject: parsedIsProject, isProtectedByDefault, maskedEnvironmentVariablesLink, + maskableRawRegex, maskableRegex, projectFullPath, projectId, diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index 1350837476b..f86dc2e2e30 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -3,6 +3,9 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import { FILTERED_SEARCH } from '~/filtered_search/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql'; addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true); @@ -13,3 +16,7 @@ initFilteredSearch({ }); projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MERGE_REQUEST, + query: searchUserProjectsWithMergeRequestsEnabled, +}); diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js index b526fce6f7b..951941cc83d 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -1,3 +1,14 @@ import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MILESTONE } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserGroupsAndProjects from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql'; projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MILESTONE, + query: searchUserGroupsAndProjects, + extractProjects: (data) => [ + ...(data?.user?.groups?.nodes ?? []), + ...(data?.projects?.nodes ?? []), + ], +}); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index bf0147ca885..40b4c289ab0 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -4,6 +4,9 @@ import { FILTERED_SEARCH } from '~/filtered_search/constants'; import { initBulkUpdateSidebar } from '~/issuable'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserGroupProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql'; const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_'; @@ -17,3 +20,8 @@ initFilteredSearch({ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MERGE_REQUEST, + query: searchUserGroupProjectsWithMergeRequestsEnabled, + extractProjects: (data) => data?.group?.projects?.nodes, +}); diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index 85ca52f633e..e650a48bc2a 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -10,6 +10,8 @@ export const ONE_COL_WIDTH = 180; export const STAGE_VIEW = 'stage'; export const LAYER_VIEW = 'layer'; + +export const SKIP_RETRY_MODAL_KEY = 'skip_retry_modal'; export const VIEW_TYPE_KEY = 'pipeline_graph_view_type'; export const SINGLE_JOB = 'single_job'; @@ -20,3 +22,5 @@ export const BRIDGE_KIND = 'BRIDGE'; export const ACTION_FAILURE = 'action_failure'; export const IID_FAILURE = 'missing_iid'; + +export const RETRY_ACTION_TITLE = 'Retry'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 1a05710a13e..aa46d2ba1a1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -44,6 +44,11 @@ export default { required: false, default: () => ({}), }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, type: { type: String, required: false, @@ -181,9 +186,11 @@ export default { :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :show-links="showJobLinks" + :skip-retry-modal="skipRetryModal" :type="$options.pipelineTypeConstants.UPSTREAM" :view-type="viewType" @error="onError" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> </template> <template #main> @@ -210,11 +217,13 @@ export default { :highlighted-jobs="highlightedJobs" :is-stage-view="isStageView" :job-hovered="hoveredJobName" + :skip-retry-modal="skipRetryModal" :source-job-hovered="hoveredSourceJobName" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipeline.id" :user-permissions="pipeline.userPermissions" @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" @jobHover="setJob" @updateMeasurements="getMeasurements" /> @@ -228,12 +237,14 @@ export default { :config-paths="configPaths" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" + :skip-retry-modal="skipRetryModal" :show-links="showJobLinks" :type="$options.pipelineTypeConstants.DOWNSTREAM" :view-type="viewType" @downstreamHovered="setSourceJob" @pipelineExpandToggle="togglePipelineExpanded" @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" @scrollContainer="slidePipelineContainer" @error="onError" /> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 4d7596e6e16..8f76d7535f1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -8,7 +8,14 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql'; import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql'; import { reportToSentry, reportMessageToSentry } from '../../utils'; -import { ACTION_FAILURE, IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants'; +import { + ACTION_FAILURE, + IID_FAILURE, + LAYER_VIEW, + SKIP_RETRY_MODAL_KEY, + STAGE_VIEW, + VIEW_TYPE_KEY, +} from './constants'; import PipelineGraph from './graph_component.vue'; import GraphViewSelector from './graph_view_selector.vue'; import { @@ -53,6 +60,7 @@ export default { currentViewType: STAGE_VIEW, canRefetchHeaderPipeline: false, pipeline: null, + skipRetryModal: false, showAlert: false, showLinks: false, }; @@ -206,8 +214,8 @@ export default { if (!this.pipelineIid) { this.reportFailure({ type: IID_FAILURE, skipSentry: true }); } - toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); + this.skipRetryModal = Boolean(JSON.parse(localStorage.getItem(SKIP_RETRY_MODAL_KEY))); }, errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); @@ -259,6 +267,9 @@ export default { updateShowLinksState(val) { this.showLinks = val; }, + setSkipRetryModal() { + this.skipRetryModal = true; + }, updateViewType(type) { this.currentViewType = type; }, @@ -293,10 +304,12 @@ export default { :config-paths="configPaths" :pipeline="pipeline" :computed-pipeline-info="getPipelineInfo()" + :skip-retry-modal="skipRetryModal" :show-links="showLinks" :view-type="graphViewType" @error="reportFailure" @refreshPipelineGraph="refreshPipelineGraph" + @setSkipRetryModal="setSkipRetryModal" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4f2be27486c..19f2a37c5ff 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -1,13 +1,14 @@ <script> -import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { GlBadge, GlForm, GlFormCheckbox, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { sprintf, __ } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import ActionComponent from '../jobs_shared/action_component.vue'; import JobNameComponent from '../jobs_shared/job_name_component.vue'; -import { BRIDGE_KIND, SINGLE_JOB } from './constants'; +import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from './constants'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -35,17 +36,31 @@ import { BRIDGE_KIND, SINGLE_JOB } from './constants'; */ export default { + confirmationModalDocLink: helpPagePath('/ci/pipelines/downstream_pipelines'), i18n: { bridgeBadgeText: __('Trigger job'), unauthorizedTooltip: __('You are not authorized to run this manual job'), + confirmationModal: { + title: s__('PipelineGraph|Are you sure you want to retry %{jobName}?'), + description: s__( + 'PipelineGraph|Retrying a trigger job will create a new downstream pipeline.', + ), + linkText: s__('PipelineGraph|What is a downstream pipeline?'), + footer: __("Don't show this again"), + actionPrimary: { text: __('Retry') }, + actionCancel: { text: __('Cancel') }, + }, }, hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, CiIcon, - JobNameComponent, GlBadge, + GlForm, + GlFormCheckbox, GlLink, + GlModal, + JobNameComponent, }, directives: { GlTooltip: GlTooltipDirective, @@ -86,6 +101,11 @@ export default { required: false, default: -1, }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, sourceJobHovered: { type: String, required: false, @@ -102,6 +122,13 @@ export default { default: SINGLE_JOB, }, }, + data() { + return { + currentSkipModalValue: this.skipRetryModal, + showConfirmationModal: false, + shouldTriggerActionClick: false, + }; + }, computed: { boundary() { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; @@ -115,6 +142,12 @@ export default { hasDetails() { return this.status.hasDetails; }, + hasRetryAction() { + return Boolean(this.job?.status?.action?.title === RETRY_ACTION_TITLE); + }, + isRetryableBridge() { + return this.isBridge && this.hasRetryAction; + }, isSingleItem() { return this.type === SINGLE_JOB; }, @@ -127,6 +160,11 @@ export default { nameComponent() { return this.hasDetails ? 'gl-link' : 'div'; }, + retryTriggerJobWarningText() { + return sprintf(this.$options.i18n.confirmationModal.title, { + jobName: this.job.name, + }); + }, showStageName() { return Boolean(this.stageName); }, @@ -205,11 +243,26 @@ export default { }, ]; }, + withConfirmationModal() { + return this.isRetryableBridge && !this.skipRetryModal; + }, + }, + watch: { + skipRetryModal(val) { + this.currentSkipModalValue = val; + this.shouldTriggerActionClick = false; + }, }, errorCaptured(err, _vm, info) { reportToSentry('job_item', `error: ${err}, info: ${info}`); }, methods: { + handleConfirmationModalPreferences() { + if (this.currentSkipModalValue) { + this.$emit('setSkipRetryModal'); + localStorage.setItem(SKIP_RETRY_MODAL_KEY, String(this.currentSkipModalValue)); + } + }, hideTooltips() { this.$root.$emit(BV_HIDE_TOOLTIP); }, @@ -227,6 +280,15 @@ export default { pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); }, + executePendingAction() { + this.shouldTriggerActionClick = true; + }, + showActionConfirmationModal() { + this.showConfirmationModal = true; + }, + toggleSkipRetryModalCheckbox() { + this.currentSkipModalValue = !this.currentSkipModalValue; + }, }, }; </script> @@ -276,8 +338,12 @@ export default { :link="status.action.path" :action-icon="status.action.icon" class="gl-mr-1" + :should-trigger-click="shouldTriggerActionClick" + :with-confirmation-modal="withConfirmationModal" data-qa-selector="job_action_button" + @actionButtonClicked="handleConfirmationModalPreferences" @pipelineActionRequestComplete="pipelineActionRequestComplete" + @showActionConfirmationModal="showActionConfirmationModal" /> <action-component v-if="hasUnauthorizedManualAction" @@ -287,5 +353,28 @@ export default { :link="`unauthorized-${computedJobId}`" class="gl-mr-1" /> + <gl-modal + v-if="showConfirmationModal" + ref="modal" + v-model="showConfirmationModal" + modal-id="action-confirmation-modal" + :title="retryTriggerJobWarningText" + :action-cancel="$options.i18n.confirmationModal.actionCancel" + :action-primary="$options.i18n.confirmationModal.actionPrimary" + @primary="executePendingAction" + @close="handleConfirmationModalPreferences" + @hide="handleConfirmationModalPreferences" + > + <p class="gl-mb-1">{{ $options.i18n.confirmationModal.description }}</p> + <gl-link :href="$options.confirmationModalDocLink" target="_blank">{{ + $options.i18n.confirmationModal.linkText + }}</gl-link> + <div class="gl-mt-4 gl-display-flex"> + <gl-form> + <gl-form-checkbox class="gl-min-h-0" @input="toggleSkipRetryModalCheckbox" /> + </gl-form> + <p class="gl-m-0">{{ $options.i18n.confirmationModal.footer }}</p> + </div> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index b06c2f15042..02e426064c9 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -36,6 +36,11 @@ export default { type: Boolean, required: true, }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, type: { type: String, required: true, @@ -229,8 +234,10 @@ export default { :pipeline="currentPipeline" :computed-pipeline-info="getPipelineLayers(pipeline.id)" :show-links="showLinks" + :skip-retry-modal="skipRetryModal" :is-linked-pipeline="true" :view-type="graphViewType" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> </div> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 4aec28295bd..ffd0fec2ca8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -53,6 +53,11 @@ export default { required: false, default: () => ({}), }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, sourceJobHovered: { type: String, required: false, @@ -164,6 +169,7 @@ export default { v-if="singleJobExists(group)" :job="group.jobs[0]" :job-hovered="jobHovered" + :skip-retry-modal="skipRetryModal" :source-job-hovered="sourceJobHovered" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" @@ -174,6 +180,7 @@ export default { 'gl-transition-duration-slow gl-transition-timing-function-ease', ]" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> <div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> <job-group-dropdown diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue index 387b01aee7e..7020bfc1e65 100644 --- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue +++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue @@ -39,6 +39,16 @@ export default { type: String, required: true, }, + withConfirmationModal: { + type: Boolean, + required: false, + default: false, + }, + shouldTriggerClick: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -52,6 +62,14 @@ export default { return `${actionIconDash} js-icon-${actionIconDash}`; }, }, + watch: { + shouldTriggerClick(flag) { + if (flag && this.withConfirmationModal) { + this.executeAction(); + this.$emit('actionButtonClicked'); + } + }, + }, errorCaptured(err, _vm, info) { reportToSentry('action_component', `error: ${err}, info: ${info}`); }, @@ -63,6 +81,13 @@ export default { * */ onClickAction() { + if (this.withConfirmationModal) { + this.$emit('showActionConfirmationModal'); + } else { + this.executeAction(); + } + }, + executeAction() { this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`); this.isDisabled = true; this.isLoading = true; @@ -91,6 +116,7 @@ export default { <template> <gl-button :id="`js-ci-action-${link}`" + ref="button" :class="cssClass" :disabled="isDisabled" class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql new file mode 100644 index 00000000000..578914dbbaf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql @@ -0,0 +1,18 @@ +query searchUserGroupProjectsWithMergeRequestsEnabled($fullPath: ID!, $search: String) { + group(fullPath: $fullPath) { + id + projects( + search: $search + withMergeRequestsEnabled: true + includeSubgroups: true + sort: ACTIVITY_DESC + ) { + nodes { + id + name + nameWithNamespace + webUrl + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql new file mode 100644 index 00000000000..8fe92cf7c6c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql @@ -0,0 +1,21 @@ +query searchUserGroupsAndProjects($username: String!, $search: String) { + projects(sort: "latest_activity_desc", membership: true) { + nodes { + id + name + nameWithNamespace + webUrl + } + } + + user(username: $username) { + id + groups(search: $search) { + nodes { + id + name + webUrl + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql new file mode 100644 index 00000000000..44ebf755728 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql @@ -0,0 +1,15 @@ +query searchUserProjectsWithMergeRequestsEnabled($search: String) { + projects( + search: $search + membership: true + withMergeRequestsEnabled: true + sort: "latest_activity_desc" + ) { + nodes { + id + name + nameWithNamespace + webUrl + } + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ade58ca0970..332e465914b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController before_action :disable_query_limiting, only: [:usage_data] + before_action do + push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development) + end + feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned :general, :reporting, :metrics_and_profiling, :network, :preferences, :update, :reset_health_check_token diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 78e3ffa4af9..3562ee55c36 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -11,6 +11,10 @@ module Groups before_action :push_licensed_features, only: [:show] before_action :assign_variables_to_gon, only: [:show] + before_action do + push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development) + end + feature_category :continuous_integration urgency :low diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index b330aacf3e9..5ec1f606ef2 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -12,6 +12,10 @@ module Projects before_action :check_builds_available! before_action :define_variables + before_action do + push_frontend_feature_flag(:ci_remove_character_limitation_raw_masked_var, type: :development) + end + helper_method :highlight_badge feature_category :continuous_integration diff --git a/app/helpers/ci/variables_helper.rb b/app/helpers/ci/variables_helper.rb index 84572363a8d..a492c48e58c 100644 --- a/app/helpers/ci/variables_helper.rb +++ b/app/helpers/ci/variables_helper.rb @@ -47,6 +47,10 @@ module Ci ] end + def ci_variable_maskable_raw_regex + Ci::Maskable::MASK_AND_RAW_REGEX.inspect.sub('\\A', '^').sub('\\z', '$')[1...-1] + end + def ci_variable_maskable_regex Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(%r{^/}, '').sub(%r{/[a-z]*$}, '').gsub('\/', '/') end diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb index 62be0150ee0..f8f6693c122 100644 --- a/app/models/concerns/ci/maskable.rb +++ b/app/models/concerns/ci/maskable.rb @@ -12,10 +12,30 @@ module Ci # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~' # * Absolutely no fun is allowed REGEX = %r{\A[a-zA-Z0-9_+=/@:.~-]{8,}\z}.freeze + # * Single line + # * No spaces + # * Minimal length of 8 characters + # * Some fun is allowed + MASK_AND_RAW_REGEX = %r{\A\S{8,}\z}.freeze included do validates :masked, inclusion: { in: [true, false] } - validates :value, format: { with: REGEX }, if: :masked? + validates :value, format: { with: REGEX }, if: :masked_and_expanded? + validates :value, format: { with: MASK_AND_RAW_REGEX }, if: :masked_and_raw? + end + + def masked_and_raw? + return false unless Feature.enabled?(:ci_remove_character_limitation_raw_masked_var) + return false unless self.class.method_defined?(:raw) + + masked? && raw? + end + + def masked_and_expanded? + return masked? unless Feature.enabled?(:ci_remove_character_limitation_raw_masked_var) + return masked? unless self.class.method_defined?(:raw) + + masked? && !raw? end def to_runner_variable diff --git a/app/models/repository.rb b/app/models/repository.rb index e939a4eb56b..3fd7b6126d8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -601,16 +601,10 @@ class Repository cache_method_asymmetrically :has_visible_content? def avatar - if Feature.enabled?(:readme_from_gitaly) - Gitlab::GitalyClient.allow_n_plus_1_calls do - avatar_path_gitaly - end - else - # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/38327 - Gitlab::GitalyClient.allow_n_plus_1_calls do - if tree = file_on_head(:avatar) - tree.path - end + # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/38327 + Gitlab::GitalyClient.allow_n_plus_1_calls do + if tree = file_on_head(:avatar) + tree.path end end end @@ -637,11 +631,7 @@ class Repository end def readme_path - if Feature.enabled?(:readme_from_gitaly) - readme_path_gitaly - else - head_tree&.readme_path - end + head_tree&.readme_path end cache_method :readme_path @@ -1249,41 +1239,6 @@ class Repository container.full_path, container: container) end - - def readme_path_gitaly - # (?i) to enable case-insensitive mode - # - # Note: `Gitlab::FileDetector::PATTERNS[:readme]#to_s` won't work because of - # incompatibility of regex engines between Rails and Gitaly. - pattern = "(?i)#{Gitlab::FileDetector::PATTERNS[:readme].source}" - - readmes = fetch_file_paths_from_gitaly(pattern) - - choose_readme_to_display(readmes) - end - - def avatar_path_gitaly - # Note: `Gitlab::FileDetector::PATTERNS[:avatar]#to_s` won't work because of - # incompatibility of regex engines between Rails and Gitaly. - pattern = Gitlab::FileDetector::PATTERNS[:avatar].source - - fetch_file_paths_from_gitaly(pattern, limit: 1).first - end - - def fetch_file_paths_from_gitaly(pattern, limit: 0) - return [] if empty? || root_ref.nil? - - search_files_by_regexp(pattern, root_ref, limit: limit) - end - - # Extracted from Tree#readme_path - def choose_readme_to_display(readmes) - previewable_readme = readmes.find { |name| Gitlab::MarkupHelper.previewable?(name) } - - return previewable_readme if previewable_readme - - readmes.find { |name| Gitlab::MarkupHelper.plain?(name) } - end end Repository.prepend_mod_with('Repository') diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index be4fa19019f..8aaa09b7862 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -17,6 +17,7 @@ is_group: is_group.to_s, group_id: @group&.id || '', group_path: @group&.full_path, + maskable_raw_regex: ci_variable_maskable_raw_regex, maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s, aws_logo_svg_path: image_path('aws_logo.svg'), diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 97fb35b28ab..677e2bd6007 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -10,7 +10,7 @@ - if current_user .page-title-controls.ml-0.mb-3.ml-sm-auto.mb-sm-0 - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _("merge request"), with_feature_enabled: 'merge_requests', type: :merge_requests + = render 'shared/new_project_item_vue_select' .top-area = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index bc8e3e6ab69..2556791da12 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -8,9 +8,7 @@ - if current_user .page-title-controls - = render 'shared/new_project_item_select', - path: '-/milestones/new', label: _('Milestone'), - include_groups: true, type: :milestones + = render 'shared/new_project_item_vue_select' - if @milestone_states.any? { |name, count| count > 0 } .top-area @@ -22,9 +20,7 @@ = render 'shared/empty_states/milestones_tab', active_tab: params[:state] do - if current_user .page-title-controls - = render 'shared/new_project_item_select', - path: '-/milestones/new', label: _('Milestone'), - include_groups: true, type: :milestones + = render 'shared/new_project_item_vue_select' - else .milestones %ul.content-list @@ -35,6 +31,4 @@ = render 'shared/empty_states/milestones' do - if current_user .page-title-controls - = render 'shared/new_project_item_select', - path: '-/milestones/new', label: _('Milestone'), - include_groups: true, type: :milestones + = render 'shared/new_project_item_vue_select' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 92f6c896e7b..f2d0cfc42fd 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -10,7 +10,7 @@ - if @can_bulk_update = render_if_exists 'projects/merge_requests/bulk_update_button' - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _("merge request"), type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true + = render 'shared/new_project_item_vue_select' = render 'shared/issuable/search_bar', type: :merge_requests - if @can_bulk_update diff --git a/app/views/shared/_new_project_item_vue_select.html.haml b/app/views/shared/_new_project_item_vue_select.html.haml index 24d275c4975..9ea99df106e 100644 --- a/app/views/shared/_new_project_item_vue_select.html.haml +++ b/app/views/shared/_new_project_item_vue_select.html.haml @@ -1,2 +1,2 @@ - if any_projects?(@projects) - .js-new-resource-dropdown + .js-new-resource-dropdown{ data: { group_id: @group&.id, full_path: @group&.full_path, username: @current_user&.username } } diff --git a/config/feature_flags/development/readme_from_gitaly.yml b/config/feature_flags/development/ci_remove_character_limitation_raw_masked_var.yml index 6e440e928f1..bd293de9962 100644 --- a/config/feature_flags/development/readme_from_gitaly.yml +++ b/config/feature_flags/development/ci_remove_character_limitation_raw_masked_var.yml @@ -1,8 +1,8 @@ --- -name: readme_from_gitaly -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108609 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387703 -milestone: '15.8' +name: ci_remove_character_limitation_raw_masked_var +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109008 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388414 +milestone: '15.9' type: development -group: group::source code +group: group::pipeline authoring default_enabled: false diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e2727684b12..581436ae28d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14801,6 +14801,9 @@ msgstr "" msgid "Don't show again" msgstr "" +msgid "Don't show this again" +msgstr "" + msgid "Done" msgstr "" @@ -30695,6 +30698,15 @@ msgstr "" msgid "PipelineEditor|Waiting for CI content to load..." msgstr "" +msgid "PipelineGraph|Are you sure you want to retry %{jobName}?" +msgstr "" + +msgid "PipelineGraph|Retrying a trigger job will create a new downstream pipeline." +msgstr "" + +msgid "PipelineGraph|What is a downstream pipeline?" +msgstr "" + msgid "PipelineScheduleIntervalPattern|Custom (%{linkStart}Learn more.%{linkEnd})" msgstr "" diff --git a/package.json b/package.json index 246a022496f..7cd85dec640 100644 --- a/package.json +++ b/package.json @@ -125,8 +125,8 @@ "dropzone": "^4.2.0", "editorconfig": "^0.15.3", "emoji-regex": "^10.0.0", - "esbuild": "0.15.18", - "esbuild-loader": "^2.20.0", + "esbuild": "0.17.4", + "esbuild-loader": "^2.21.0", "fast-mersenne-twister": "1.0.2", "file-loader": "^6.2.0", "fuzzaldrin-plus": "^0.6.0", diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index a146a6987bc..34bab9dffd0 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -36,11 +36,16 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl end it 'shows projects only with merge requests feature enabled', :js do - click_button 'Toggle project select' + click_button 'Select project to create merge request' + wait_for_requests - page.within('.select2-results') do + page.within('[data-testid="new-resource-dropdown"]') do expect(page).to have_content(project.full_name) expect(page).not_to have_content(project_with_disabled_merge_requests.full_name) + + find_button(project.full_name).click + + expect(page).to have_link("New merge request in #{project.name}") end end end diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index a9f23f90bb1..3b197bbf009 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -37,19 +37,13 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do describe 'new milestones dropdown', :js do it 'takes user to a new milestone page', :js do - click_button 'Toggle project select' + click_button 'Select project to create milestone' - page.within('.select2-results') do - first('.select2-result-label').click + page.within('[data-testid="new-resource-dropdown"]') do + click_button group.name + click_link "New milestone in #{group.name}" end - a_el = find('.js-new-project-item-link') - - expect(a_el).to have_content('New Milestone in ') - expect(a_el).to have_no_content('New New Milestone in ') - - a_el.click - expect(page).to have_current_path(new_group_milestone_path(group), ignore_query: true) end end diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index a37c40f50e0..e123e223ae5 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -98,13 +98,9 @@ RSpec.describe 'Group empty states', feature_category: :subgroups do end it "the new #{issuable_name} button opens a project dropdown" do - click_button 'Toggle project select' + click_button "Select project to create #{issuable_name}" - if issuable == :issue - expect(page).to have_button project.name - else - expect(page).to have_selector('.ajax-project-dropdown') - end + expect(page).to have_button project.name end end end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 8a3401d0572..bbb7d322b9a 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -77,9 +77,9 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf end it 'shows projects only with merge requests feature enabled', :js do - find('.js-new-project-item-link').click + click_button 'Select project to create merge request' - page.within('.select2-results') do + page.within('[data-testid="new-resource-dropdown"]') do expect(page).to have_content(project.name_with_namespace) expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace) end @@ -95,7 +95,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf visit path expect(page).to have_selector('.empty-state') - expect(page).to have_link('Select project to create merge request') + expect(page).to have_button('Select project to create merge request') expect(page).to have_selector('.issues-filters') end @@ -105,7 +105,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf visit path expect(page).to have_selector('.empty-state') - expect(page).to have_link('Select project to create merge request') + expect(page).to have_button('Select project to create merge request') expect(page).to have_selector('.issues-filters') end end diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js index 71826628ffd..d8bb03404f3 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -21,6 +21,8 @@ describe('Ci variable modal', () => { let trackingSpy; const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + const maskableRawRegex = '^\\S{8,}$'; + const mockVariables = mockVariablesWithScopes(instanceString); const defaultProvide = { @@ -30,8 +32,12 @@ describe('Ci variable modal', () => { awsTipLearnLink: '/learn-link', containsVariableReferenceLink: '/reference', environmentScopeLink: '/help/environments', + glFeatures: { + ciRemoveCharacterLimitationRawMaskedVar: true, + }, isProtectedByDefault: false, maskedEnvironmentVariablesLink: '/variables-link', + maskableRawRegex, maskableRegex, }; @@ -423,6 +429,54 @@ describe('Ci variable modal', () => { describe('Validations', () => { const maskError = 'This variable can not be masked.'; + describe('when the variable is raw', () => { + const [variable] = mockVariables; + const validRawMaskedVariable = { + ...variable, + value: 'd$%^asdsadas', + masked: false, + raw: true, + }; + + describe('and FF is enabled', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validRawMaskedVariable }, + }); + }); + + it('should not show an error with symbols', async () => { + await findMaskedVariableCheckbox().trigger('click'); + + expect(findModal().text()).not.toContain(maskError); + }); + + it('should not show an error when length is less than 8', async () => { + await findValueField().vm.$emit('input', 'a'); + await findMaskedVariableCheckbox().trigger('click'); + + expect(findModal().text()).toContain(maskError); + }); + }); + + describe('and FF is disabled', () => { + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validRawMaskedVariable }, + provide: { glFeatures: { ciRemoveCharacterLimitationRawMaskedVar: false } }, + }); + }); + + it('should show an error with symbols', async () => { + await findMaskedVariableCheckbox().trigger('click'); + + expect(findModal().text()).toContain(maskError); + }); + }); + }); + describe('when the mask state is invalid', () => { beforeEach(async () => { const [variable] = mockVariables; diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index 225a095fb3b..e3eea503b46 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -13,18 +13,22 @@ describe('pipeline graph action component', () => { const findButton = () => wrapper.findComponent(GlButton); const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]'); + const defaultProps = { + tooltipText: 'bar', + link: 'foo', + actionIcon: 'cancel', + }; + + const createComponent = ({ props } = {}) => { + wrapper = mount(ActionComponent, { + propsData: { ...defaultProps, ...props }, + }); + }; + beforeEach(() => { mock = new MockAdapter(axios); mock.onPost('foo.json').reply(HTTP_STATUS_OK); - - wrapper = mount(ActionComponent, { - propsData: { - tooltipText: 'bar', - link: 'foo', - actionIcon: 'cancel', - }, - }); }); afterEach(() => { @@ -32,31 +36,39 @@ describe('pipeline graph action component', () => { wrapper.destroy(); }); - it('should render the provided title as a bootstrap tooltip', () => { - expect(findTooltipWrapper().attributes('title')).toBe('bar'); - }); + describe('render', () => { + beforeEach(() => { + createComponent(); + }); - it('should update bootstrap tooltip when title changes', async () => { - wrapper.setProps({ tooltipText: 'changed' }); + it('should render the provided title as a bootstrap tooltip', () => { + expect(findTooltipWrapper().attributes('title')).toBe('bar'); + }); - await nextTick(); - expect(findTooltipWrapper().attributes('title')).toBe('changed'); - }); + it('should update bootstrap tooltip when title changes', async () => { + wrapper.setProps({ tooltipText: 'changed' }); - it('should render an svg', () => { - expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); - expect(wrapper.find('svg').exists()).toBe(true); + await nextTick(); + expect(findTooltipWrapper().attributes('title')).toBe('changed'); + }); + + it('should render an svg', () => { + expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); + expect(wrapper.find('svg').exists()).toBe(true); + }); }); describe('on click', () => { - it('emits `pipelineActionRequestComplete` after a successful request', async () => { - jest.spyOn(wrapper.vm, '$emit'); + beforeEach(() => { + createComponent(); + }); + it('emits `pipelineActionRequestComplete` after a successful request', async () => { findButton().trigger('click'); await waitForPromises(); - expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); + expect(wrapper.emitted().pipelineActionRequestComplete).toHaveLength(1); }); it('renders a loading icon while waiting for request', async () => { @@ -66,4 +78,40 @@ describe('pipeline graph action component', () => { expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); }); }); + + describe('when has a confirmation modal', () => { + beforeEach(() => { + createComponent({ props: { withConfirmationModal: true, shouldTriggerClick: false } }); + }); + + describe('and a first click is initiated', () => { + beforeEach(async () => { + findButton().trigger('click'); + + await waitForPromises(); + }); + + it('emits `showActionConfirmationModal` event', () => { + expect(wrapper.emitted().showActionConfirmationModal).toHaveLength(1); + }); + + it('does not emit `pipelineActionRequestComplete` event', () => { + expect(wrapper.emitted().pipelineActionRequestComplete).toBeUndefined(); + }); + }); + + describe('and the `shouldTriggerClick` value becomes true', () => { + beforeEach(async () => { + await wrapper.setProps({ shouldTriggerClick: true }); + }); + + it('does not emit `showActionConfirmationModal` event', () => { + expect(wrapper.emitted().showActionConfirmationModal).toBeUndefined(); + }); + + it('emits `actionButtonClicked` event', () => { + expect(wrapper.emitted().actionButtonClicked).toHaveLength(1); + }); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js index 2abb5f7dc58..d2f55ecefc0 100644 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -96,6 +96,16 @@ describe('graph component', () => { }); }); + describe('when column request an update to the retry confirmation modal', () => { + beforeEach(() => { + findStageColumns().at(0).vm.$emit('setSkipRetryModal'); + }); + + it('setSkipRetryModal is emitted', () => { + expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1); + }); + }); + describe('when links are present', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 587a3c67168..5f5303a5339 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -199,6 +199,22 @@ describe('Pipeline graph wrapper', () => { }); }); + describe('events', () => { + beforeEach(async () => { + createComponentWithApollo(); + await waitForPromises(); + }); + describe('when receiving `setSkipRetryModal` event', () => { + it('passes down `skipRetryModal` value as true', async () => { + expect(getGraph().props('skipRetryModal')).toBe(false); + + await getGraph().vm.$emit('setSkipRetryModal'); + + expect(getGraph().props('skipRetryModal')).toBe(true); + }); + }); + }); + describe('when there is an error with an action in the graph', () => { beforeEach(async () => { createComponentWithApollo(); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 05776ec0706..7b3ee276b91 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -1,7 +1,11 @@ +import MockAdapter from 'axios-mock-adapter'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlModal } from '@gitlab/ui'; import JobItem from '~/pipelines/components/graph/job_item.vue'; +import axios from '~/lib/utils/axios_utils'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { delayedJob, @@ -9,36 +13,66 @@ import { mockJobWithoutDetails, mockJobWithUnauthorizedAction, triggerJob, + triggerJobWithRetryAction, } from './mock_data'; describe('pipeline graph job item', () => { + useLocalStorageSpy(); + let wrapper; + let mockAxios; const findJobWithoutLink = () => wrapper.findByTestId('job-without-link'); const findJobWithLink = () => wrapper.findByTestId('job-with-link'); const findActionComponent = () => wrapper.findByTestId('ci-action-component'); const findBadge = () => wrapper.findComponent(GlBadge); + const findJobLink = () => wrapper.findByTestId('job-with-link'); + const findModal = () => wrapper.findComponent(GlModal); + + const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary'); + const clickOnModalCancelBtn = () => findModal().vm.$emit('hide'); + const clickOnModalCloseBtn = () => findModal().vm.$emit('close'); + + const myCustomClass1 = 'my-class-1'; + const myCustomClass2 = 'my-class-2'; - const createWrapper = (propsData) => { + const defaultProps = { + job: mockJob, + }; + + const createWrapper = ({ props, data } = {}) => { wrapper = extendedWrapper( mount(JobItem, { - propsData, + data() { + return { + ...data, + }; + }, + propsData: { + ...defaultProps, + ...props, + }, }), ); }; const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500'; + beforeEach(() => { + mockAxios = new MockAdapter(axios); + }); + afterEach(() => { + mockAxios.restore(); wrapper.destroy(); }); describe('name with link', () => { it('should render the job name and status with a link', async () => { - createWrapper({ job: mockJob }); + createWrapper(); await nextTick(); - const link = wrapper.find('a'); + const link = findJobLink(); expect(link.attributes('href')).toBe(mockJob.status.detailsPath); @@ -53,15 +87,17 @@ describe('pipeline graph job item', () => { describe('name without link', () => { beforeEach(() => { createWrapper({ - job: mockJobWithoutDetails, - cssClassJobName: 'css-class-job-name', - jobHovered: 'test', + props: { + job: mockJobWithoutDetails, + cssClassJobName: 'css-class-job-name', + jobHovered: 'test', + }, }); }); it('should render status and name', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); - expect(wrapper.find('a').exists()).toBe(false); + expect(findJobLink().exists()).toBe(false); expect(wrapper.text()).toBe(mockJobWithoutDetails.name); }); @@ -73,7 +109,7 @@ describe('pipeline graph job item', () => { describe('action icon', () => { it('should render the action icon', () => { - createWrapper({ job: mockJob }); + createWrapper(); const actionComponent = findActionComponent(); @@ -83,7 +119,11 @@ describe('pipeline graph job item', () => { }); it('should render disabled action icon when user cannot run the action', () => { - createWrapper({ job: mockJobWithUnauthorizedAction }); + createWrapper({ + props: { + job: mockJobWithUnauthorizedAction, + }, + }); const actionComponent = findActionComponent(); @@ -96,13 +136,15 @@ describe('pipeline graph job item', () => { describe('job style', () => { beforeEach(() => { createWrapper({ - job: mockJob, - cssClassJobName: 'css-class-job-name', + props: { + job: mockJob, + cssClassJobName: 'css-class-job-name', + }, }); }); it('should render provided class name', () => { - expect(wrapper.find('a').classes()).toContain('css-class-job-name'); + expect(findJobLink().classes()).toContain('css-class-job-name'); }); it('does not show a badge on the job item', () => { @@ -117,11 +159,13 @@ describe('pipeline graph job item', () => { describe('status label', () => { it('should not render status label when it is not provided', () => { createWrapper({ - job: { - id: 4258, - name: 'test', - status: { - icon: 'status_success', + props: { + job: { + id: 4258, + name: 'test', + status: { + icon: 'status_success', + }, }, }, }); @@ -131,13 +175,15 @@ describe('pipeline graph job item', () => { it('should not render status label when it is provided', () => { createWrapper({ - job: { - id: 4259, - name: 'test', - status: { - icon: 'status_success', - label: 'success', - tooltip: 'success', + props: { + job: { + id: 4259, + name: 'test', + status: { + icon: 'status_success', + label: 'success', + tooltip: 'success', + }, }, }, }); @@ -149,7 +195,9 @@ describe('pipeline graph job item', () => { describe('for delayed job', () => { it('displays remaining time in tooltip', () => { createWrapper({ - job: delayedJob, + props: { + job: delayedJob, + }, }); expect(findJobWithLink().attributes('title')).toBe( @@ -161,7 +209,11 @@ describe('pipeline graph job item', () => { describe('trigger job', () => { describe('card', () => { beforeEach(() => { - createWrapper({ job: triggerJob }); + createWrapper({ + props: { + job: triggerJob, + }, + }); }); it('shows a badge on the job item', () => { @@ -182,7 +234,12 @@ describe('pipeline graph job item', () => { `( `trigger job should stay highlighted when downstream is expanded`, ({ job, jobName, expanded, link }) => { - createWrapper({ job, pipelineExpanded: { jobName, expanded } }); + createWrapper({ + props: { + job, + pipelineExpanded: { jobName, expanded }, + }, + }); const findJobEl = link ? findJobWithLink : findJobWithoutLink; expect(findJobEl().classes()).toContain(triggerActiveClass); @@ -196,7 +253,12 @@ describe('pipeline graph job item', () => { `( `trigger job should not be highlighted when downstream is not expanded`, ({ job, jobName, expanded, link }) => { - createWrapper({ job, pipelineExpanded: { jobName, expanded } }); + createWrapper({ + props: { + job, + pipelineExpanded: { jobName, expanded }, + }, + }); const findJobEl = link ? findJobWithLink : findJobWithoutLink; expect(findJobEl().classes()).not.toContain(triggerActiveClass); @@ -208,60 +270,182 @@ describe('pipeline graph job item', () => { describe('job classes', () => { it('job class is shown', () => { createWrapper({ - job: mockJob, - cssClassJobName: 'my-class', + props: { + job: mockJob, + cssClassJobName: 'my-class', + }, }); - expect(wrapper.find('a').classes()).toContain('my-class'); + const jobLinkEl = findJobLink(); + + expect(jobLinkEl.classes()).toContain('my-class'); - expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass); + expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); }); it('job class is shown, along with hover', () => { createWrapper({ - job: mockJob, - cssClassJobName: 'my-class', - sourceJobHovered: mockJob.name, + props: { + job: mockJob, + cssClassJobName: 'my-class', + sourceJobHovered: mockJob.name, + }, }); - expect(wrapper.find('a').classes()).toContain('my-class'); - expect(wrapper.find('a').classes()).toContain(triggerActiveClass); + const jobLinkEl = findJobLink(); + + expect(jobLinkEl.classes()).toContain('my-class'); + expect(jobLinkEl.classes()).toContain(triggerActiveClass); }); it('multiple job classes are shown', () => { createWrapper({ - job: mockJob, - cssClassJobName: ['my-class-1', 'my-class-2'], + props: { + job: mockJob, + cssClassJobName: [myCustomClass1, myCustomClass2], + }, }); - expect(wrapper.find('a').classes()).toContain('my-class-1'); - expect(wrapper.find('a').classes()).toContain('my-class-2'); + const jobLinkEl = findJobLink(); + + expect(jobLinkEl.classes()).toContain(myCustomClass1); + expect(jobLinkEl.classes()).toContain(myCustomClass2); - expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass); + expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); }); it('multiple job classes are shown conditionally', () => { createWrapper({ - job: mockJob, - cssClassJobName: { 'my-class-1': true, 'my-class-2': true }, + props: { + job: mockJob, + cssClassJobName: { [myCustomClass1]: true, [myCustomClass2]: true }, + }, }); - expect(wrapper.find('a').classes()).toContain('my-class-1'); - expect(wrapper.find('a').classes()).toContain('my-class-2'); + const jobLinkEl = findJobLink(); + + expect(jobLinkEl.classes()).toContain(myCustomClass1); + expect(jobLinkEl.classes()).toContain(myCustomClass2); - expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass); + expect(jobLinkEl.classes()).not.toContain(triggerActiveClass); }); it('multiple job classes are shown, along with a hover', () => { createWrapper({ - job: mockJob, - cssClassJobName: ['my-class-1', 'my-class-2'], - sourceJobHovered: mockJob.name, + props: { + job: mockJob, + cssClassJobName: [myCustomClass1, myCustomClass2], + sourceJobHovered: mockJob.name, + }, }); - expect(wrapper.find('a').classes()).toContain('my-class-1'); - expect(wrapper.find('a').classes()).toContain('my-class-2'); - expect(wrapper.find('a').classes()).toContain(triggerActiveClass); + const jobLinkEl = findJobLink(); + + expect(jobLinkEl.classes()).toContain(myCustomClass1); + expect(jobLinkEl.classes()).toContain(myCustomClass2); + expect(jobLinkEl.classes()).toContain(triggerActiveClass); + }); + }); + + describe('confirmation modal', () => { + describe('when clicking on the action component', () => { + it.each` + skipRetryModal | exists | visibilityText + ${false} | ${true} | ${'shows'} + ${true} | ${false} | ${'hides'} + `( + '$visibilityText the modal when `skipRetryModal` is $skipRetryModal', + async ({ exists, skipRetryModal }) => { + createWrapper({ + props: { + skipRetryModal, + job: triggerJobWithRetryAction, + }, + }); + await findActionComponent().trigger('click'); + + expect(findModal().exists()).toBe(exists); + }, + ); + }); + + describe('when showing the modal', () => { + it.each` + buttonName | shouldTriggerActionClick | actionBtn + ${'primary'} | ${true} | ${clickOnModalPrimaryBtn} + ${'cancel'} | ${false} | ${clickOnModalCancelBtn} + ${'close'} | ${false} | ${clickOnModalCloseBtn} + `( + 'clicking on $buttonName will pass down shouldTriggerActionClick as $shouldTriggerActionClick to the action component', + async ({ shouldTriggerActionClick, actionBtn }) => { + createWrapper({ + props: { + skipRetryModal: false, + job: triggerJobWithRetryAction, + }, + }); + await findActionComponent().trigger('click'); + + await actionBtn(); + + expect(findActionComponent().props().shouldTriggerClick).toBe(shouldTriggerActionClick); + }, + ); + }); + + describe('when not checking the "do not show this again" checkbox', () => { + it.each` + actionName | actionBtn + ${'closing'} | ${clickOnModalCloseBtn} + ${'cancelling'} | ${clickOnModalCancelBtn} + ${'confirming'} | ${clickOnModalPrimaryBtn} + `( + 'does not emit any event and will not modify localstorage on $actionName', + async ({ actionBtn }) => { + createWrapper({ + props: { + skipRetryModal: false, + job: triggerJobWithRetryAction, + }, + }); + await findActionComponent().trigger('click'); + await actionBtn(); + + expect(wrapper.emitted().setSkipRetryModal).toBeUndefined(); + expect(localStorage.setItem).not.toHaveBeenCalled(); + }, + ); + }); + + describe('when checking the "do not show this again" checkbox', () => { + it.each` + actionName | actionBtn + ${'closing'} | ${clickOnModalCloseBtn} + ${'cancelling'} | ${clickOnModalCancelBtn} + ${'confirming'} | ${clickOnModalPrimaryBtn} + `( + 'emits "setSkipRetryModal" and set local storage key on $actionName the modal', + async ({ actionBtn }) => { + // We are passing the checkbox as a slot to the GlModal. + // The way GlModal is mounted, we can neither click on the box + // or emit an event directly. We therefore set the data property + // as it would be if the box was checked. + createWrapper({ + data: { + currentSkipModalValue: true, + }, + props: { + skipRetryModal: false, + job: triggerJobWithRetryAction, + }, + }); + await findActionComponent().trigger('click'); + await actionBtn(); + + expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1); + expect(localStorage.setItem).toHaveBeenCalledWith('skip_retry_modal', 'true'); + }, + ); }); }); }); diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 6124d67af09..fc6dfe9ec03 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -1,5 +1,9 @@ import { unwrapPipelineData } from '~/pipelines/components/graph/utils'; -import { BUILD_KIND, BRIDGE_KIND } from '~/pipelines/components/graph/constants'; +import { + BUILD_KIND, + BRIDGE_KIND, + RETRY_ACTION_TITLE, +} from '~/pipelines/components/graph/constants'; export const mockPipelineResponse = { data: { @@ -1038,3 +1042,16 @@ export const triggerJob = { action: null, }, }; + +export const triggerJobWithRetryAction = { + ...triggerJob, + status: { + ...triggerJob.status, + action: { + icon: 'retry', + title: RETRY_ACTION_TITLE, + path: '/root/ci-mock/builds/4259/retry', + method: 'post', + }, + }, +}; diff --git a/spec/helpers/ci/variables_helper_spec.rb b/spec/helpers/ci/variables_helper_spec.rb new file mode 100644 index 00000000000..d032e7f9087 --- /dev/null +++ b/spec/helpers/ci/variables_helper_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::VariablesHelper, feature_category: :pipeline_authoring do + describe '#ci_variable_maskable_raw_regex' do + it 'converts to a javascript regex' do + expect(helper.ci_variable_maskable_raw_regex).to eq("^\\S{8,}$") + end + end +end diff --git a/spec/lib/gitlab/repository_cache/preloader_spec.rb b/spec/lib/gitlab/repository_cache/preloader_spec.rb index 71244dd41ed..21628481fed 100644 --- a/spec/lib/gitlab/repository_cache/preloader_spec.rb +++ b/spec/lib/gitlab/repository_cache/preloader_spec.rb @@ -18,8 +18,8 @@ RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_redis_cachin # Warm the cache but use a different model so they are not memoized repos = Project.id_in(projects).order(:id).map(&:repository) - allow(repos[0]).to receive(:readme_path_gitaly).and_return('README.txt') - allow(repos[1]).to receive(:readme_path_gitaly).and_return('README.md') + allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt') + allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md') repos.map(&:exists?) repos.map(&:readme_path) diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb index 2b13fc21fe8..e2c1e08fe49 100644 --- a/spec/models/concerns/ci/maskable_spec.rb +++ b/spec/models/concerns/ci/maskable_spec.rb @@ -2,15 +2,16 @@ require 'spec_helper' -RSpec.describe Ci::Maskable do +RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do let(:variable) { build(:ci_variable) } describe 'masked value validations' do subject { variable } - context 'when variable is masked' do + context 'when variable is masked and expanded' do before do subject.masked = true + subject.raw = false end it { is_expected.not_to allow_value('hello').for(:value) } @@ -20,6 +21,70 @@ RSpec.describe Ci::Maskable do it { is_expected.to allow_value('helloworld').for(:value) } end + context 'when method :raw is not defined' do + let(:test_var_class) do + Struct.new(:masked?) do + include ActiveModel::Validations + include Ci::Maskable + end + end + + let(:variable) { test_var_class.new(true) } + + it 'evaluates masked variables as expanded' do + expect(subject).not_to be_masked_and_raw + expect(subject).to be_masked_and_expanded + end + end + + context 'when the ci_remove_character_limitation_raw_masked_var FF is disabled' do + before do + stub_feature_flags(ci_remove_character_limitation_raw_masked_var: false) + end + + context 'when variable is masked and raw' do + before do + subject.masked = true + subject.raw = true + end + + it { is_expected.not_to allow_value('hello').for(:value) } + it { is_expected.not_to allow_value('hello world').for(:value) } + it { is_expected.not_to allow_value('hello$VARIABLEworld').for(:value) } + it { is_expected.not_to allow_value('hello\rworld').for(:value) } + it { is_expected.not_to allow_value('hello&&&world').for(:value) } + it { is_expected.not_to allow_value('helloworld!!!!').for(:value) } + it { is_expected.to allow_value('helloworld').for(:value) } + end + + context 'when variable is not masked' do + before do + subject.masked = false + end + + it { is_expected.to allow_value('hello').for(:value) } + it { is_expected.to allow_value('hello world').for(:value) } + it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) } + it { is_expected.to allow_value('hello\rworld').for(:value) } + it { is_expected.to allow_value('helloworld').for(:value) } + end + end + + context 'when variable is masked and raw' do + before do + subject.masked = true + subject.raw = true + end + + it { is_expected.not_to allow_value('hello').for(:value) } + it { is_expected.not_to allow_value('hello world').for(:value) } + it { is_expected.to allow_value('hello\rworld').for(:value) } + it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) } + it { is_expected.to allow_value('helloworld!!!').for(:value) } + it { is_expected.to allow_value('hell******world').for(:value) } + it { is_expected.to allow_value('helloworld123').for(:value) } + end + context 'when variable is not masked' do before do subject.masked = false @@ -33,40 +98,70 @@ RSpec.describe Ci::Maskable do end end - describe 'REGEX' do - subject { Ci::Maskable::REGEX } + describe 'Regexes' do + context 'with MASK_AND_RAW_REGEX' do + subject { Ci::Maskable::MASK_AND_RAW_REGEX } - it 'does not match strings shorter than 8 letters' do - expect(subject.match?('hello')).to eq(false) - end + it 'does not match strings shorter than 8 letters' do + expect(subject.match?('hello')).to eq(false) + end - it 'does not match strings with spaces' do - expect(subject.match?('hello world')).to eq(false) - end + it 'does not match strings with spaces' do + expect(subject.match?('hello world')).to eq(false) + end - it 'does not match strings with shell variables' do - expect(subject.match?('hello$VARIABLEworld')).to eq(false) - end + it 'does not match strings that span more than one line' do + string = <<~EOS + hello + world + EOS - it 'does not match strings with escape characters' do - expect(subject.match?('hello\rworld')).to eq(false) + expect(subject.match?(string)).to eq(false) + end + + it 'matches valid strings' do + expect(subject.match?('hello$VARIABLEworld')).to eq(true) + expect(subject.match?('Hello+World_123/@:-~.')).to eq(true) + expect(subject.match?('hello\rworld')).to eq(true) + expect(subject.match?('HelloWorld%#^')).to eq(true) + end end - it 'does not match strings that span more than one line' do - string = <<~EOS - hello - world - EOS + context 'with REGEX' do + subject { Ci::Maskable::REGEX } - expect(subject.match?(string)).to eq(false) - end + it 'does not match strings shorter than 8 letters' do + expect(subject.match?('hello')).to eq(false) + end - it 'does not match strings using unsupported characters' do - expect(subject.match?('HelloWorld%#^')).to eq(false) - end + it 'does not match strings with spaces' do + expect(subject.match?('hello world')).to eq(false) + end + + it 'does not match strings with shell variables' do + expect(subject.match?('hello$VARIABLEworld')).to eq(false) + end + + it 'does not match strings with escape characters' do + expect(subject.match?('hello\rworld')).to eq(false) + end + + it 'does not match strings that span more than one line' do + string = <<~EOS + hello + world + EOS - it 'matches valid strings' do - expect(subject.match?('Hello+World_123/@:-~.')).to eq(true) + expect(subject.match?(string)).to eq(false) + end + + it 'does not match strings using unsupported characters' do + expect(subject.match?('HelloWorld%#^')).to eq(false) + end + + it 'matches valid strings' do + expect(subject.match?('Hello+World_123/@:-~.')).to eq(true) + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3485c877373..f1528ac4de3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2562,52 +2562,28 @@ RSpec.describe Repository, feature_category: :source_code_management do describe '#avatar' do let(:project) { create(:project, :repository) } - it 'returns nil if repo is empty' do - allow(repository).to receive(:empty).and_return(true) + it 'returns nil if repo does not exist' do + allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository) expect(repository.avatar).to be_nil end it 'returns the first avatar file found in the repository' do - expect(repository).to receive(:search_files_by_regexp).and_return(['logo.png']) + expect(repository).to receive(:file_on_head) + .with(:avatar) + .and_return(double(:tree, path: 'logo.png')) expect(repository.avatar).to eq('logo.png') end it 'caches the output' do - expect(repository).to receive(:search_files_by_regexp).once.and_return(['logo.png']) + expect(repository).to receive(:file_on_head) + .with(:avatar) + .once + .and_return(double(:tree, path: 'logo.png')) 2.times { expect(repository.avatar).to eq('logo.png') } end - - context 'when feature flag readme_from_gitaly is disabled' do - before do - stub_feature_flags(readme_from_gitaly: false) - end - - it 'returns nil if repo does not exist' do - allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository) - - expect(repository.avatar).to be_nil - end - - it 'returns the first avatar file found in the repository' do - expect(repository).to receive(:file_on_head) - .with(:avatar) - .and_return(double(:tree, path: 'logo.png')) - - expect(repository.avatar).to eq('logo.png') - end - - it 'caches the output' do - expect(repository).to receive(:file_on_head) - .with(:avatar) - .once - .and_return(double(:tree, path: 'logo.png')) - - 2.times { expect(repository.avatar).to eq('logo.png') } - end - end end describe '#expire_exists_cache' do @@ -2732,26 +2708,12 @@ RSpec.describe Repository, feature_category: :source_code_management do end it 'caches the response' do - expect(repository).to receive(:search_files_by_regexp).and_call_original.once + expect(repository.head_tree).to receive(:readme_path).and_call_original.once 2.times do expect(repository.readme_path).to eq("README.md") end end - - context 'when "readme_from_gitaly" FF is disabled' do - before do - stub_feature_flags(readme_from_gitaly: false) - end - - it 'caches the response' do - expect(repository.head_tree).to receive(:readme_path).and_call_original.once - - 2.times do - expect(repository.readme_path).to eq("README.md") - end - end - end end end end diff --git a/yarn.lock b/yarn.lock index 6a2d6feafcd..ad5d8588457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1065,15 +1065,225 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== -"@esbuild/android-arm@0.15.18": - version "0.15.18" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" - integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw== - -"@esbuild/linux-loong64@0.15.18": - version "0.15.18" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" - integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ== +"@esbuild/android-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" + integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== + +"@esbuild/android-arm64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.4.tgz#0a900a7e448cc038ae5a751255257fc67163ed32" + integrity sha512-91VwDrl4EpxBCiG6h2LZZEkuNvVZYJkv2T9gyLG/mhGG1qrM7i5SwUcg/hlSPnL/4hDT0TFcF35/XMGSn0bemg== + +"@esbuild/android-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" + integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== + +"@esbuild/android-arm@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.4.tgz#fe32ce82eb6064d3dc13c0d8ca0e440bbc776c93" + integrity sha512-R9GCe2xl2XDSc2XbQB63mFiFXHIVkOP+ltIxICKXqUPrFX97z6Z7vONCLQM1pSOLGqfLrGi3B7nbhxmFY/fomg== + +"@esbuild/android-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" + integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== + +"@esbuild/android-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.4.tgz#6ae1056f6ecf1963c1d076cf5f0109b52d8049f6" + integrity sha512-mGSqhEPL7029XL7QHNPxPs15JVa02hvZvysUcyMP9UXdGFwncl2WU0bqx+Ysgzd+WAbv8rfNa73QveOxAnAM2w== + +"@esbuild/darwin-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" + integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== + +"@esbuild/darwin-arm64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.4.tgz#5064d81ee5b8d646a5b7cc3e53c98cb983c4af55" + integrity sha512-tTyJRM9dHvlMPt1KrBFVB5OW1kXOsRNvAPtbzoKazd5RhD5/wKlXk1qR2MpaZRYwf4WDMadt0Pv0GwxB41CVow== + +"@esbuild/darwin-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" + integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== + +"@esbuild/darwin-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.4.tgz#67f0213b3333248b32a97a7fc3fee880c2157674" + integrity sha512-phQuC2Imrb3TjOJwLN8EO50nb2FHe8Ew0OwgZDH1SV6asIPGudnwTQtighDF2EAYlXChLoMJwqjAp4vAaACq6w== + +"@esbuild/freebsd-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" + integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== + +"@esbuild/freebsd-arm64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.4.tgz#8eaaa126d9ff24822c730f06a71ac2d1091dc1c2" + integrity sha512-oH6JUZkocgmjzzYaP5juERLpJQSwazdjZrTPgLRmAU2bzJ688x0vfMB/WTv4r58RiecdHvXOPC46VtsMy/mepg== + +"@esbuild/freebsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" + integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== + +"@esbuild/freebsd-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.4.tgz#314eff900a71abf64d4e5bea31e430d8ebd78d79" + integrity sha512-U4iWGn/9TrAfpAdfd56eO0pRxIgb0a8Wj9jClrhT8hvZnOnS4dfMPW7o4fn15D/KqoiVYHRm43jjBaTt3g/2KA== + +"@esbuild/linux-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" + integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== + +"@esbuild/linux-arm64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.4.tgz#5bed6bb8eb1d331644f8b31c87b8df57f204e84e" + integrity sha512-UkGfQvYlwOaeYJzZG4cLV0hCASzQZnKNktRXUo3/BMZvdau40AOz9GzmGA063n1piq6VrFFh43apRDQx8hMP2w== + +"@esbuild/linux-arm@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" + integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== + +"@esbuild/linux-arm@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.4.tgz#6eaa41f37e231d113da715a1d9cc820e5523aeb6" + integrity sha512-S2s9xWTGMTa/fG5EyMGDeL0wrWVgOSQcNddJWgu6rG1NCSXJHs76ZP9AsxjB3f2nZow9fWOyApklIgiTGZKhiw== + +"@esbuild/linux-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" + integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== + +"@esbuild/linux-ia32@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.4.tgz#3fc352bb54e0959fda273cd2253b1c72ca41b8c2" + integrity sha512-3lqFi4VFo/Vwvn77FZXeLd0ctolIJH/uXkH3yNgEk89Eh6D3XXAC9/iTPEzeEpsNE5IqGIsFa5Z0iPeOh25IyA== + +"@esbuild/linux-loong64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" + integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== + +"@esbuild/linux-loong64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.4.tgz#86d54f690be53669cd2a38a5333ecf2608c11189" + integrity sha512-HqpWZkVslDHIwdQ9D+gk7NuAulgQvRxF9no54ut/M55KEb3mi7sQS3GwpPJzSyzzP0UkjQVN7/tbk88/CaX4EQ== + +"@esbuild/linux-mips64el@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" + integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== + +"@esbuild/linux-mips64el@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.4.tgz#3dbd897bd8f047fef35e69bd253b8f07ca7fe483" + integrity sha512-d/nMCKKh/SVDbqR9ju+b78vOr0tNXtfBjcp5vfHONCCOAL9ad8gN9dC/u+UnH939pz7wO+0u/x9y1MaZcb/lKA== + +"@esbuild/linux-ppc64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" + integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== + +"@esbuild/linux-ppc64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.4.tgz#defaff6db9a60f08936fc0c59e0eabfb1055968a" + integrity sha512-lOD9p2dmjZcNiTU+sGe9Nn6G3aYw3k0HBJies1PU0j5IGfp6tdKOQ6mzfACRFCqXjnBuTqK7eTYpwx09O5LLfg== + +"@esbuild/linux-riscv64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" + integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== + +"@esbuild/linux-riscv64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.4.tgz#270a09f6f4205a8a8c8ed3c7dbabdcebaafa8a84" + integrity sha512-mTGnwWwVshAjGsd8rP+K6583cPDgxOunsqqldEYij7T5/ysluMHKqUIT4TJHfrDFadUwrghAL6QjER4FeqQXoA== + +"@esbuild/linux-s390x@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" + integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== + +"@esbuild/linux-s390x@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.4.tgz#197695bece68f514dcdcc286562b5d48c5dad5f9" + integrity sha512-AQYuUGp50XM29/N/dehADxvc2bUqDcoqrVuijop1Wv72SyxT6dDB9wjUxuPZm2HwIM876UoNNBMVd+iX/UTKVQ== + +"@esbuild/linux-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" + integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== + +"@esbuild/linux-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.4.tgz#db50cdfb071c0d367025c1c98563aab1318f800e" + integrity sha512-+AsFBwKgQuhV2shfGgA9YloxLDVjXgUEWZum7glR5lLmV94IThu/u2JZGxTgjYby6kyXEx8lKOqP5rTEVBR0Rw== + +"@esbuild/netbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" + integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== + +"@esbuild/netbsd-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.4.tgz#e4d5d8022f8eddbd7d9899d58265915444f46f3b" + integrity sha512-zD1TKYX9553OiLS/qkXPMlWoELYkH/VkzRYNKEU+GwFiqkq0SuxsKnsCg5UCdxN3cqd+1KZ8SS3R+WG/Hxy2jQ== + +"@esbuild/openbsd-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" + integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== + +"@esbuild/openbsd-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.4.tgz#9b770e1e7745824cbe155f5a742fc781855a7e68" + integrity sha512-PY1NjEsLRhPEFFg1AV0/4Or/gR+q2dOb9s5rXcPuCjyHRzbt8vnHJl3vYj+641TgWZzTFmSUnZbzs1zwTzjeqw== + +"@esbuild/sunos-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" + integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== + +"@esbuild/sunos-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.4.tgz#4c6d2290f8bf39ab9284f5a1b9a2210858e2d6e6" + integrity sha512-B3Z7s8QZQW9tKGleMRXvVmwwLPAUoDCHs4WZ2ElVMWiortLJFowU1NjAhXOKjDgC7o9ByeVcwyOlJ+F2r6ZgmQ== + +"@esbuild/win32-arm64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" + integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== + +"@esbuild/win32-arm64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.4.tgz#424954b6d598f40e2c5a0d85e3af07147fb41909" + integrity sha512-0HCu8R3mY/H5V7N6kdlsJkvrT591bO/oRZy8ztF1dhgNU5xD5tAh5bKByT1UjTGjp/VVBsl1PDQ3L18SfvtnBQ== + +"@esbuild/win32-ia32@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" + integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== + +"@esbuild/win32-ia32@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.4.tgz#2c94e9c3a82c779d3f07b3fb5c482a2e3fecedb1" + integrity sha512-VUjhVDQycse1gLbe06pC/uaA0M+piQXJpdpNdhg8sPmeIZZqu5xPoGWVCmcsOO2gaM2cywuTYTHkXRozo3/Nkg== + +"@esbuild/win32-x64@0.16.17": + version "0.16.17" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" + integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== + +"@esbuild/win32-x64@0.17.4": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.4.tgz#9b7760cdc77678bdbc5b582fae2cf3de449df048" + integrity sha512-0kLAjs+xN5OjhTt/aUA6t48SfENSCKgGPfExADYTOo/UCn0ivxos9/anUVeSfg+L+2O9xkFxvJXIJfG+Q4sYSg== "@eslint/eslintrc@^1.4.1": version "1.4.1" @@ -5294,145 +5504,73 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild-android-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5" - integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA== - -esbuild-android-arm64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04" - integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ== - -esbuild-darwin-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410" - integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg== - -esbuild-darwin-arm64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337" - integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA== - -esbuild-freebsd-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2" - integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA== - -esbuild-freebsd-arm64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635" - integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA== - -esbuild-linux-32@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce" - integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg== - -esbuild-linux-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c" - integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw== - -esbuild-linux-arm64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d" - integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug== - -esbuild-linux-arm@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc" - integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA== - -esbuild-linux-mips64le@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb" - integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ== - -esbuild-linux-ppc64le@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507" - integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w== - -esbuild-linux-riscv64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6" - integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg== - -esbuild-linux-s390x@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb" - integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ== - -esbuild-loader@^2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.20.0.tgz#28fcff0142fa7bd227512d69f31e9a6e202bb88f" - integrity sha512-dr+j8O4w5RvqZ7I4PPB4EIyVTd679EBQnMm+JBB7av+vu05Zpje2IpK5N3ld1VWa+WxrInIbNFAg093+E1aRsA== - dependencies: - esbuild "^0.15.6" +esbuild-loader@^2.21.0: + version "2.21.0" + resolved "https://registry.yarnpkg.com/esbuild-loader/-/esbuild-loader-2.21.0.tgz#2698a3e565b0db2bb19a3dd91c2b6c9aad526c80" + integrity sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g== + dependencies: + esbuild "^0.16.17" joycon "^3.0.1" json5 "^2.2.0" loader-utils "^2.0.0" tapable "^2.2.0" - webpack-sources "^2.2.0" - -esbuild-netbsd-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998" - integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg== - -esbuild-openbsd-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8" - integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ== - -esbuild-sunos-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971" - integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw== - -esbuild-windows-32@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3" - integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ== - -esbuild-windows-64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0" - integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw== - -esbuild-windows-arm64@0.15.18: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7" - integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ== - -esbuild@0.15.18, esbuild@^0.15.6: - version "0.15.18" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d" - integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q== + webpack-sources "^1.4.3" + +esbuild@0.17.4: + version "0.17.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.4.tgz#af4f8f78604c67f8e6afbdee36a3f4211ecfc859" + integrity sha512-zBn9MeCwT7W5F1a3lXClD61ip6vQM+H8Msb0w8zMT4ZKBpDg+rFAraNyWCDelB/2L6M3g6AXHPnsyvjMFnxtFw== + optionalDependencies: + "@esbuild/android-arm" "0.17.4" + "@esbuild/android-arm64" "0.17.4" + "@esbuild/android-x64" "0.17.4" + "@esbuild/darwin-arm64" "0.17.4" + "@esbuild/darwin-x64" "0.17.4" + "@esbuild/freebsd-arm64" "0.17.4" + "@esbuild/freebsd-x64" "0.17.4" + "@esbuild/linux-arm" "0.17.4" + "@esbuild/linux-arm64" "0.17.4" + "@esbuild/linux-ia32" "0.17.4" + "@esbuild/linux-loong64" "0.17.4" + "@esbuild/linux-mips64el" "0.17.4" + "@esbuild/linux-ppc64" "0.17.4" + "@esbuild/linux-riscv64" "0.17.4" + "@esbuild/linux-s390x" "0.17.4" + "@esbuild/linux-x64" "0.17.4" + "@esbuild/netbsd-x64" "0.17.4" + "@esbuild/openbsd-x64" "0.17.4" + "@esbuild/sunos-x64" "0.17.4" + "@esbuild/win32-arm64" "0.17.4" + "@esbuild/win32-ia32" "0.17.4" + "@esbuild/win32-x64" "0.17.4" + +esbuild@^0.16.17: + version "0.16.17" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" + integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== optionalDependencies: - "@esbuild/android-arm" "0.15.18" - "@esbuild/linux-loong64" "0.15.18" - esbuild-android-64 "0.15.18" - esbuild-android-arm64 "0.15.18" - esbuild-darwin-64 "0.15.18" - esbuild-darwin-arm64 "0.15.18" - esbuild-freebsd-64 "0.15.18" - esbuild-freebsd-arm64 "0.15.18" - esbuild-linux-32 "0.15.18" - esbuild-linux-64 "0.15.18" - esbuild-linux-arm "0.15.18" - esbuild-linux-arm64 "0.15.18" - esbuild-linux-mips64le "0.15.18" - esbuild-linux-ppc64le "0.15.18" - esbuild-linux-riscv64 "0.15.18" - esbuild-linux-s390x "0.15.18" - esbuild-netbsd-64 "0.15.18" - esbuild-openbsd-64 "0.15.18" - esbuild-sunos-64 "0.15.18" - esbuild-windows-32 "0.15.18" - esbuild-windows-64 "0.15.18" - esbuild-windows-arm64 "0.15.18" + "@esbuild/android-arm" "0.16.17" + "@esbuild/android-arm64" "0.16.17" + "@esbuild/android-x64" "0.16.17" + "@esbuild/darwin-arm64" "0.16.17" + "@esbuild/darwin-x64" "0.16.17" + "@esbuild/freebsd-arm64" "0.16.17" + "@esbuild/freebsd-x64" "0.16.17" + "@esbuild/linux-arm" "0.16.17" + "@esbuild/linux-arm64" "0.16.17" + "@esbuild/linux-ia32" "0.16.17" + "@esbuild/linux-loong64" "0.16.17" + "@esbuild/linux-mips64el" "0.16.17" + "@esbuild/linux-ppc64" "0.16.17" + "@esbuild/linux-riscv64" "0.16.17" + "@esbuild/linux-s390x" "0.16.17" + "@esbuild/linux-x64" "0.16.17" + "@esbuild/netbsd-x64" "0.16.17" + "@esbuild/openbsd-x64" "0.16.17" + "@esbuild/sunos-x64" "0.16.17" + "@esbuild/win32-arm64" "0.16.17" + "@esbuild/win32-ia32" "0.16.17" + "@esbuild/win32-x64" "0.16.17" escalade@^3.1.1: version "3.1.1" @@ -11167,7 +11305,7 @@ sortablejs@^1.10.2, sortablejs@^1.9.0: resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290" integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A== -source-list-map@^2.0.0, source-list-map@^2.0.1: +source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== @@ -12641,14 +12779,6 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" - integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - webpack-stats-plugin@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.3.1.tgz#1103c39a305a4e6ba15d5078db84bc0b35447417" |