diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 12:13:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 12:13:12 +0300 |
commit | 6170bdc060501ecf6f817a530b3dc9f2e39ad4c3 (patch) | |
tree | c0e92981040c3aa0b2b78de816ee51cfb53ce661 | |
parent | 136651d7cb69357d2823adefac430df389e81e17 (diff) |
Add latest changes from gitlab-org/gitlab@master
65 files changed, 675 insertions, 630 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 82d7c149cd4..8ba690533ca 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -4,7 +4,7 @@ include: - local: .gitlab/ci/global.gitlab-ci.yml - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common - ref: 1.0.1 + ref: 1.2.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml @@ -22,11 +22,12 @@ include: image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3 .bundle-install: - extends: .qa-cache + extends: + - .gitlab-qa-install + - .qa-cache variables: - RUN_WITH_BUNDLE: "true" # adds 'bundle exec' prefix for gitlab-qa commands in pipeline-common job templates - before_script: - - cd qa && bundle install + RUN_WITH_BUNDLE: "true" # installs and runs gitlab-qa via bundler + QA_PATH: qa .update-script: script: @@ -35,15 +36,8 @@ include: - echo "Running - '$QA_COMMAND'" - eval "$QA_COMMAND" -.manual-qa: - stage: test - allow_failure: true - needs: - - trigger-omnibus - .qa: extends: - - .use-docker-in-docker - .qa-base - .bundle-install - .gitlab-qa-report @@ -90,6 +84,19 @@ download-knapsack-report: - qa/knapsack/ee-*.json expire_in: 1 day +# e2e test jobs run on separate runner which has separate cache setup +cache-gems: + extends: + - .ruby-image + - .bundle-install + - .qa-cache-push + - .rules:prepare + stage: .pre + tags: + - e2e + script: + - echo "Populated qa cache" + # ========================================== # Test stage # ========================================== @@ -104,8 +111,11 @@ download-knapsack-report: _ee:quarantine: extends: - .qa - - .manual-qa - .rules:test:quarantine + needs: + - trigger-omnibus + stage: test + allow_failure: true variables: QA_RSPEC_TAGS: --tag quarantine @@ -446,7 +456,7 @@ notify-slack: - .rules:report:process-results stage: .post variables: - ALLURE_JOB_NAME: package-and-qa + ALLURE_JOB_NAME: package-and-e2e SLACK_ICON_EMOJI: ci_failing STATUS_SYM: ☠️ STATUS: failed diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 82f6411a5fa..58d2c2417af 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -69,7 +69,7 @@ qa:update-qa-cache: populate-e2e-test-vars: extends: - .qa-job-base - - .qa:rules:determine-qa-tests + - .qa:rules:determine-e2e-tests stage: prepare variables: ENV_FILE: $CI_PROJECT_DIR/qa_tests_vars.env @@ -83,7 +83,7 @@ populate-e2e-test-vars: e2e-test-pipeline-generate: extends: - - .qa:rules:determine-qa-tests + - .qa:rules:determine-e2e-tests stage: prepare when: on_success needs: @@ -99,7 +99,7 @@ e2e-test-pipeline-generate: e2e:package-and-test: extends: - - .qa:rules:package-and-qa + - .qa:rules:package-and-test stage: qa when: on_success needs: diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index ab6c0b36bfb..54f9b183919 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -162,7 +162,7 @@ allure-report-qa-blocking: - review-qa-smoke - review-qa-reliable variables: - ALLURE_JOB_NAME: review-qa-blocking + ALLURE_JOB_NAME: e2e-review-qa-blocking allure-report-qa-all: extends: @@ -170,7 +170,7 @@ allure-report-qa-all: - .review:rules:review-qa-all-report needs: ["review-qa-all"] variables: - ALLURE_JOB_NAME: review-qa-all + ALLURE_JOB_NAME: e2e-review-qa-all upload-knapsack-report: extends: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 317faeb9642..1f37e7f7cd3 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -133,7 +133,7 @@ .if-dot-com-gitlab-org-and-security-merge-request-and-qa-tests-specified: &if-dot-com-gitlab-org-and-security-merge-request-and-qa-tests-specified if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_MERGE_REQUEST_IID && $QA_TESTS' -.if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa: &if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa +.if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e: &if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_MERGE_REQUEST_IID && $QA_MANUAL_FF_PACKAGE_AND_QA' .if-dot-com-gitlab-org-and-security-tag: &if-dot-com-gitlab-org-and-security-tag @@ -916,7 +916,7 @@ - <<: *if-default-refs changes: *qa-patterns -.qa:rules:determine-qa-tests: +.qa:rules:determine-e2e-tests: rules: - <<: *if-not-ee when: never @@ -930,7 +930,7 @@ - <<: *if-force-ci allow_failure: true -.qa:rules:package-and-qa: +.qa:rules:package-and-test: rules: - <<: *if-fork-merge-request when: never @@ -940,7 +940,7 @@ when: never - <<: *if-merge-request-targeting-stable-branch allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-qa + - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e changes: *feature-flag-development-config-patterns when: manual allow_failure: true diff --git a/.gitlab/merge_request_templates/Quarantine End to End Test.md b/.gitlab/merge_request_templates/Quarantine End to End Test.md index a8d2378eee0..c088fde857a 100644 --- a/.gitlab/merge_request_templates/Quarantine End to End Test.md +++ b/.gitlab/merge_request_templates/Quarantine End to End Test.md @@ -26,7 +26,6 @@ the noise (due to constantly failing tests, flaky tests, and so on) so that new - [ ] Dequarantine test check-list - [ ] Follow the [Dequarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#dequarantining-tests). - [ ] Confirm the test consistently passes on the target GitLab environment(s). - - [ ] (Optionally) [Trigger a manual GitLab-QA pipeline](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release) against a specific GitLab environment using the `RELEASE` variable from the `package-and-qa` job of the current merge request. - [ ] To ensure a faster turnaround, ask in the `#quality` Slack channel for someone to review and merge the merge request, rather than assigning it directly. <!-- Base labels. --> diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md index d130881b8da..14130ca42c2 100644 --- a/.gitlab/merge_request_templates/Security Release.md +++ b/.gitlab/merge_request_templates/Security Release.md @@ -21,7 +21,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla - [ ] Ensure it's approved according to our [Approval Guidelines]. - [ ] Ensure it's approved by an AppSec engineer. - Please see the security release [Code reviews and Approvals] documentation for details on which AppSec team member to ping for approval. - - Trigger the [`package-and-qa` build]. The docker image generated will be used by the AppSec engineer to validate the security vulnerability has been remediated. + - Trigger the [`e2e:package-and-test` job]. The docker image generated will be used by the AppSec engineer to validate the security vulnerability has been remediated. - [ ] For a backport MR targeting a versioned stable branch (`X-Y-stable-ee`). - [ ] Milestone is set to the version this backport applies to. A closed milestone can be assigned via [quick actions]. - [ ] Ensure it's approved by a maintainer. diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index f6eeb3d533e..3e5694d0811 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -3,29 +3,7 @@ Layout/HashAlignment: Exclude: - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb' - - 'lib/gitlab/no_cache_headers.rb' - - 'lib/gitlab/sidekiq_daemon/memory_killer.rb' - - 'lib/gitlab/sidekiq_middleware/server_metrics.rb' - - 'lib/gitlab/slash_commands/presenters/base.rb' - - 'lib/gitlab/template/gitignore_template.rb' - - 'lib/gitlab/visibility_level.rb' - - 'lib/product_analytics/event_params.rb' - - 'lib/sidebars/projects/menus/infrastructure_menu.rb' - - 'lib/tasks/gitlab/import_export/export.rake' - - 'lib/tasks/gitlab/import_export/import.rake' - - 'lib/tasks/tanuki_emoji.rake' - - 'spec/controllers/concerns/product_analytics_tracking_spec.rb' - - 'spec/controllers/concerns/redis_tracking_spec.rb' - - 'spec/controllers/import/bitbucket_controller_spec.rb' - - 'spec/controllers/oauth/token_info_controller_spec.rb' - - 'spec/controllers/omniauth_callbacks_controller_spec.rb' - - 'spec/controllers/projects/artifacts_controller_spec.rb' - - 'spec/controllers/projects/feature_flags_controller_spec.rb' - - 'spec/controllers/projects/grafana_api_controller_spec.rb' - 'spec/controllers/projects/merge_requests_controller_spec.rb' - - 'spec/controllers/projects/pipeline_schedules_controller_spec.rb' - - 'spec/controllers/projects/registry/tags_controller_spec.rb' - - 'spec/controllers/projects/service_desk_controller_spec.rb' - 'spec/lib/backup/gitaly_backup_spec.rb' - 'spec/lib/banzai/filter/repository_link_filter_spec.rb' - 'spec/lib/gitlab/asciidoc_spec.rb' diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue new file mode 100644 index 00000000000..821d78b0496 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue @@ -0,0 +1,118 @@ +<script> +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { isEqual, get, isEmpty } from 'lodash'; +import { + CONTAINER_CLEANUP_POLICY_TITLE, + CONTAINER_CLEANUP_POLICY_DESCRIPTION, + FETCH_SETTINGS_ERROR_MESSAGE, + UNAVAILABLE_FEATURE_TITLE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + UNAVAILABLE_USER_FEATURE_TEXT, + UNAVAILABLE_ADMIN_FEATURE_TEXT, +} from '~/packages_and_registries/settings/project/constants'; +import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; + +import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue'; + +export default { + components: { + GlAlert, + GlSprintf, + GlLink, + ContainerExpirationPolicyForm, + }, + inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'], + i18n: { + CONTAINER_CLEANUP_POLICY_TITLE, + CONTAINER_CLEANUP_POLICY_DESCRIPTION, + UNAVAILABLE_FEATURE_TITLE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + FETCH_SETTINGS_ERROR_MESSAGE, + }, + apollo: { + containerExpirationPolicy: { + query: expirationPolicyQuery, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: (data) => data.project?.containerExpirationPolicy, + result({ data }) { + this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) }; + }, + error(e) { + this.fetchSettingsError = e; + }, + }, + }, + data() { + return { + fetchSettingsError: false, + containerExpirationPolicy: null, + workingCopy: {}, + }; + }, + computed: { + isEnabled() { + return this.containerExpirationPolicy || this.enableHistoricEntries; + }, + showDisabledFormMessage() { + return !this.isEnabled && !this.fetchSettingsError; + }, + unavailableFeatureMessage() { + return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT; + }, + isEdited() { + if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) { + return false; + } + return !isEqual(this.containerExpirationPolicy, this.workingCopy); + }, + }, + methods: { + restoreOriginal() { + this.workingCopy = { ...this.containerExpirationPolicy }; + }, + }, +}; +</script> + +<template> + <div data-testid="container-expiration-policy-project-settings"> + <h4 data-testid="title">{{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</h4> + <p data-testid="description"> + <gl-sprintf :message="$options.i18n.CONTAINER_CLEANUP_POLICY_DESCRIPTION"> + <template #link="{ content }"> + <gl-link :href="helpPagePath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + <container-expiration-policy-form + v-if="isEnabled" + v-model="workingCopy" + :is-loading="$apollo.queries.containerExpirationPolicy.loading" + :is-edited="isEdited" + @reset="restoreOriginal" + /> + <template v-else> + <gl-alert + v-if="showDisabledFormMessage" + :dismissible="false" + :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE" + variant="tip" + > + {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }} + + <gl-sprintf :message="unavailableFeatureMessage"> + <template #link="{ content }"> + <gl-link :href="adminSettingsPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> + <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false"> + <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" /> + </gl-alert> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js new file mode 100644 index 00000000000..6332f79ebb8 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js @@ -0,0 +1,39 @@ +import { GlToast } from '@gitlab/ui'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import Translate from '~/vue_shared/translate'; +import CleanupImageTags from './components/cleanup_image_tags.vue'; +import { apolloProvider } from './graphql/index'; + +Vue.use(GlToast); +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-registry-settings-cleanup-image-tags'); + if (!el) { + return null; + } + const { + isAdmin, + enableHistoricEntries, + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, + helpPagePath, + } = el.dataset; + return new Vue({ + el, + apolloProvider, + provide: { + isAdmin: parseBoolean(isAdmin), + enableHistoricEntries: parseBoolean(enableHistoricEntries), + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, + helpPagePath, + }, + render(createElement) { + return createElement(CleanupImageTags, {}); + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/settings/packages_and_registries/cleanup_tags/index.js b/app/assets/javascripts/pages/projects/settings/packages_and_registries/cleanup_tags/index.js new file mode 100644 index 00000000000..acd5d3febff --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/packages_and_registries/cleanup_tags/index.js @@ -0,0 +1,5 @@ +import registrySettingsCleanupTagsApp from '~/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle'; +import initSettingsPanels from '~/settings_panels'; + +registrySettingsCleanupTagsApp(); +initSettingsPanels(); diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js index 62d6bd42d51..ccca9ca8250 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js @@ -130,7 +130,7 @@ export const releaseUpdateMutatationVariables = (state, getters) => { projectPath: state.projectPath, tagName: state.release.tagName, name, - releasedAt: state.release.releasedAt, + releasedAt: getters.releasedAtChanged ? state.release.releasedAt : null, description: state.includeTagNotes ? getters.formattedReleaseNotes : state.release.description, @@ -167,3 +167,6 @@ export const formattedReleaseNotes = ({ includeTagNotes, release: { description includeTagNotes && tagNotes ? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${tagNotes}\n` : description; + +export const releasedAtChanged = ({ originalReleasedAt, release }) => + originalReleasedAt !== release.releasedAt; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js index ea794f91f66..34361f84a5a 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js @@ -14,7 +14,7 @@ export default { description: '', milestones: [], groupMilestones: [], - releasedAt: new Date(), + releasedAt: state.originalReleasedAt, assets: { links: [], }, @@ -29,6 +29,7 @@ export default { state.isFetchingRelease = false; state.release = data; state.originalRelease = Object.freeze(cloneDeep(state.release)); + state.originalReleasedAt = state.originalRelease.releasedAt; }, [types.RECEIVE_RELEASE_ERROR](state, error) { state.fetchError = error; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js index cb447cf9aaf..11a2f9df59b 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js @@ -61,4 +61,5 @@ export default ({ tagNotes: '', includeTagNotes: false, existingRelease: null, + originalReleasedAt: new Date(), }); diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index d5600a793c2..69a732a8e6e 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -32,7 +32,6 @@ import WorkItemTitle from './work_item_title.vue'; import WorkItemDescription from './work_item_description.vue'; import WorkItemAssignees from './work_item_assignees.vue'; import WorkItemLabels from './work_item_labels.vue'; -import WorkItemWeight from './work_item_weight.vue'; import WorkItemInformation from './work_item_information.vue'; export default { @@ -53,7 +52,7 @@ export default { WorkItemLabels, WorkItemTitle, WorkItemState, - WorkItemWeight, + WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'), WorkItemInformation, LocalStorageSync, WorkItemTypeIcon, diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue deleted file mode 100644 index 9f917fbb718..00000000000 --- a/app/assets/javascripts/work_items/components/work_item_weight.vue +++ /dev/null @@ -1,167 +0,0 @@ -<script> -import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import { __ } from '~/locale'; -import Tracking from '~/tracking'; -import { - sprintfWorkItem, - I18N_WORK_ITEM_ERROR_UPDATING, - TRACKING_CATEGORY_SHOW, -} from '../constants'; -import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; - -/* eslint-disable @gitlab/require-i18n-strings */ -const allowedKeys = [ - 'Alt', - 'ArrowDown', - 'ArrowLeft', - 'ArrowRight', - 'ArrowUp', - 'Backspace', - 'Control', - 'Delete', - 'End', - 'Enter', - 'Home', - 'Meta', - 'PageDown', - 'PageUp', - 'Tab', - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', -]; -/* eslint-enable @gitlab/require-i18n-strings */ - -export default { - inputId: 'weight-widget-input', - components: { - GlForm, - GlFormGroup, - GlFormInput, - }, - mixins: [Tracking.mixin()], - inject: ['hasIssueWeightsFeature'], - props: { - canUpdate: { - type: Boolean, - required: false, - default: false, - }, - weight: { - type: Number, - required: false, - default: undefined, - }, - workItemId: { - type: String, - required: true, - }, - workItemType: { - type: String, - required: true, - }, - }, - data() { - return { - isEditing: false, - }; - }, - computed: { - placeholder() { - return this.canUpdate && this.isEditing ? __('Enter a number') : __('None'); - }, - tracking() { - return { - category: TRACKING_CATEGORY_SHOW, - label: 'item_weight', - property: `type_${this.workItemType}`, - }; - }, - type() { - return this.canUpdate && this.isEditing ? 'number' : 'text'; - }, - }, - methods: { - blurInput() { - this.$refs.input.$el.blur(); - }, - handleFocus() { - this.isEditing = true; - }, - handleKeydown(event) { - if (!allowedKeys.includes(event.key)) { - event.preventDefault(); - } - }, - updateWeight(event) { - if (!this.canUpdate) return; - this.isEditing = false; - - const weight = Number(event.target.value); - if (this.weight === weight) { - return; - } - - this.track('updated_weight'); - this.$apollo - .mutate({ - mutation: updateWorkItemMutation, - variables: { - input: { - id: this.workItemId, - weightWidget: { - weight: event.target.value === '' ? null : weight, - }, - }, - }, - }) - .then(({ data }) => { - if (data.workItemUpdate.errors.length) { - throw new Error(data.workItemUpdate.errors.join('\n')); - } - }) - .catch((error) => { - const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType); - this.$emit('error', msg); - Sentry.captureException(error); - }); - }, - }, -}; -</script> - -<template> - <gl-form v-if="hasIssueWeightsFeature" @submit.prevent="blurInput"> - <gl-form-group - class="gl-align-items-center" - :label="__('Weight')" - :label-for="$options.inputId" - label-class="gl-pb-0! gl-overflow-wrap-break" - label-cols="3" - label-cols-lg="2" - > - <gl-form-input - :id="$options.inputId" - ref="input" - min="0" - :placeholder="placeholder" - :readonly="!canUpdate" - size="sm" - :type="type" - :value="weight" - @blur="updateWeight" - @focus="handleFocus" - @keydown="handleKeydown" - @keydown.exact.esc.stop="blurInput" - /> - </gl-form-group> - </gl-form> -</template> diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index a957c9ce9e0..3e5f63796b2 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -45,7 +45,11 @@ module SystemNoteHelper 'attention_requested' => 'user', 'attention_request_removed' => 'user', 'contact' => 'users', - 'timeline_event' => 'clock' + 'timeline_event' => 'clock', + 'relate_to_child' => 'link', + 'unrelate_from_child' => 'link', + 'relate_to_parent' => 'link', + 'unrelate_from_parent' => 'link' }.freeze def system_note_icon_name(note) diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index cc389dbe3f4..4e86036952b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -25,6 +25,7 @@ class SystemNoteMetadata < ApplicationRecord tag due_date start_date_or_due_date pinned_embed cherry_pick health_status approved unapproved status alert_issue_added relate unrelate new_alert_added severity attention_requested attention_request_removed contact timeline_event + issue_type relate_to_child unrelate_from_child relate_to_parent unrelate_from_parent ].freeze validates :note, presence: true, unless: :importing? diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb index aca98596a02..2e9775af8c2 100644 --- a/app/services/issuable_links/create_service.rb +++ b/app/services/issuable_links/create_service.rb @@ -41,7 +41,7 @@ module IssuableLinks set_link_type(link) if link.changed? && link.save - create_notes(referenced_issuable) + create_notes(link) end link @@ -124,9 +124,9 @@ module IssuableLinks :issue end - def create_notes(referenced_issuable) - SystemNoteService.relate_issuable(issuable, referenced_issuable, current_user) - SystemNoteService.relate_issuable(referenced_issuable, issuable, current_user) + def create_notes(issuable_link) + SystemNoteService.relate_issuable(issuable_link.source, issuable_link.target, current_user) + SystemNoteService.relate_issuable(issuable_link.target, issuable_link.source, current_user) end def linkable_issuables(objects) diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 92504c226fb..17c0085c252 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -152,7 +152,7 @@ From there, you can see the following actions: - Added, removed, or updated protected branches - Release was added to a project - Release was updated -- Release was deleted ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94793/) in GitLab 13.5) +- Release was deleted ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94793/) in GitLab 15.3) - Release milestone associations changed - Permission to approve merge requests by committers was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9) - Permission to approve merge requests by committers was updated. diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md index e11e516485a..500a19fe1ad 100644 --- a/doc/development/experiment_guide/index.md +++ b/doc/development/experiment_guide/index.md @@ -10,7 +10,7 @@ Experiments can be conducted by any GitLab team, most often the teams from the [Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/). Experiments are not tied to releases because they primarily target GitLab.com. -Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/#experiment-type) +Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/index.md#experiment-type) to turn the test on or off. Based on the data the experiment generates, the team decides if the experiment had a positive impact and should be made the new default, or rolled back. diff --git a/doc/development/export_csv.md b/doc/development/export_csv.md index 0f50d1438fc..42b9f868a30 100644 --- a/doc/development/export_csv.md +++ b/doc/development/export_csv.md @@ -14,7 +14,7 @@ This document lists the different implementations of CSV export in GitLab codeba | Downloading | - Query and write data in batches to a temporary file.<br>- Loads the file into memory.<br>- Sends the file to the client. | - Report available immediately. | - Large amount of data might cause request timeout.<br>- Memory intensive.<br>- Request expires when user navigates to a different page. | - [Export Chain of Custody Report](../user/compliance/compliance_report/#chain-of-custody-report)<br>- [Export License Usage File](../subscriptions/self_managed/index.md#export-your-license-usage) | | As email attachment | - Asynchronously process the query with background job.<br>- Email uses the export as an attachment. | - Asynchronous processing. | - Requires users use a different app (email) to download the CSV.<br>- Email providers may limit attachment size. | - [Export issues](../user/project/issues/csv_export.md)<br>- [Export merge requests](../user/project/merge_requests/csv_export.md) | | As downloadable link in email (*) | - Asynchronously process the query with background job.<br>- Email uses an export link. | - Asynchronous processing.<br>- Bypasses email provider attachment size limit. | - Requires users use a different app (email).<br>- Requires additional storage and cleanup. | [Export User Permissions](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) | -| Polling (non-persistent state) | - Asynchronously processes the query with the background job.<br>- Frontend(FE) polls every few seconds to check if CSV file is ready. | - Asynchronous processing.<br>- Automatically downloads to local machine on completion.<br>- In-app solution. | - Non-persistable request - request expires when user navigates to a different page.<br>- API is processed for each polling request. | [Export Vulnerabilities](../user/application_security/vulnerability_report/#export-vulnerability-details) | +| Polling (non-persistent state) | - Asynchronously processes the query with the background job.<br>- Frontend(FE) polls every few seconds to check if CSV file is ready. | - Asynchronous processing.<br>- Automatically downloads to local machine on completion.<br>- In-app solution. | - Non-persistable request - request expires when user navigates to a different page.<br>- API is processed for each polling request. | [Export Vulnerabilities](../user/application_security/vulnerability_report/index.md#export-vulnerability-details) | | Polling (persistent state) (*) | - Asynchronously processes the query with background job.<br>- Backend (BE) maintains the export state<br>- FE polls every few seconds to check status.<br>- FE shows 'Download link' when export is ready.<br>- User can download or regenerate a new report. | - Asynchronous processing.<br>- No database calls made during the polling requests (HTTP 304 status is returned until export status changes).<br>- Does not require user to stay on page until export is complete.<br>- In-app solution.<br>- Can be expanded into a generic CSV feature (such as dashboard / CSV API). | - Requires to maintain export states in DB.<br>- Does not automatically download the CSV export to local machine, requires users to select 'Download'. | [Export Merge Commits Report](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43055) | NOTE: diff --git a/doc/development/fips_compliance.md b/doc/development/fips_compliance.md index c690408ee60..ccfb8a7b471 100644 --- a/doc/development/fips_compliance.md +++ b/doc/development/fips_compliance.md @@ -578,6 +578,6 @@ Merge requests that can trigger Package and QA, can trigger a FIPS package and a Reference Architecture test pipeline. The base image used for the trigger is Ubuntu 20.04 FIPS: -1. Trigger `package-and-qa`, if not already triggered. +1. Trigger `e2e:package-and-test` job, if not already triggered. 1. On the `gitlab-omnibus-mirror` child pipeline, manually trigger `Trigger:package:fips`. 1. When the package job is complete, manually trigger the `RAT:FIPS` job. diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 66f5bfde074..f6fab6b7acc 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -103,7 +103,7 @@ When you need to revert a merge request, to get accelerated feedback, you can ad When this label is assigned, the following steps of the CI/CD pipeline are skipped: -- The `package-and-qa` job. +- The `e2e:package-and-test` job. - The `rspec:undercoverage` job. - The entire [Review Apps process](testing_guide/review_apps.md). @@ -489,7 +489,7 @@ graph RL; class 2_3-1 criticalPath; 2_3-1 --> 1-5 - 2_4-1["package-and-qa (102 minutes)"]; + 2_4-1["e2e:package-and-test (102 minutes)"]; class 2_4-1 criticalPath; click 2_4-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914305&udv=0" 2_4-1 --> 1-2 & 2_3-1 & 1-15; diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index 5ddf4e8fa73..561595d2048 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -806,7 +806,7 @@ This is the recommended approach to test Prometheus-based Service Ping. To verify your change, build a new Omnibus image from your code branch using CI/CD, download the image, and run a local container instance: -1. From your merge request, select the `qa` stage, then trigger the `package-and-qa` job. This job triggers an Omnibus +1. From your merge request, select the `qa` stage, then trigger the `e2e:package-and-test` job. This job triggers an Omnibus build in a [downstream pipeline of the `omnibus-gitlab-mirror` project](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/-/pipelines). 1. In the downstream pipeline, wait for the `gitlab-docker` job to finish. 1. Open the job logs and locate the full container name including the version. It takes the following form: `registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:<VERSION>`. diff --git a/doc/development/testing_guide/end_to_end/feature_flags.md b/doc/development/testing_guide/end_to_end/feature_flags.md index ec8012dce6a..9b72148b614 100644 --- a/doc/development/testing_guide/end_to_end/feature_flags.md +++ b/doc/development/testing_guide/end_to_end/feature_flags.md @@ -195,12 +195,10 @@ End-to-end tests should pass with a feature flag enabled before it is enabled on There are two ways to confirm that end-to-end tests pass: - If a merge request adds or edits a [feature flag definition file](../../feature_flags/index.md#feature-flag-definition-and-validation), - two `package-and-qa` jobs (`package-and-qa-ff-enabled` and `package-and-qa-ff-disabled`) are included automatically in the merge request - pipeline. One job enables the defined feature flag and the other job disables it. The jobs execute the same suite of tests to confirm - that they pass with the feature flag either enabled or disabled. -- In some cases, if `package-and-qa` hasn't been triggered automatically, or if it has run the tests with the default feature flag values - (which might not be desired), you can create a Draft MR that enables the feature flag to ensure that all E2E tests pass with the feature - flag enabled. + two `e2e:package-and-test` jobs (`ee:instance-parallel` and `ee:instance-parallel-ff-inverse`) are included automatically in the merge request pipeline. + One job runs the application with default feature flag state and another sets it to inverse value. The jobs execute the same suite of tests to confirm that they pass with the feature flag either enabled or disabled. +- In some cases, if end-to-end test jobs didn't trigger automatically, or if it has run the tests with the default feature flag values (which might not be desired), + you can create a Draft MR that enables the feature flag to ensure that all E2E tests pass with the feature flag enabled and disabled. ### Troubleshooting end-to-end test failures with feature flag enabled @@ -216,7 +214,7 @@ If enabling the feature flag results in E2E test failures, you can browse the ar ### Test execution during feature development If an end-to-end test enables a feature flag, the end-to-end test suite can be used to test changes in a merge request -by running the `package-and-qa` job in the merge request pipeline. If the feature flag and relevant changes have already been merged, you can confirm that the tests +by running the `e2e:package-and-test` job in the merge request pipeline. If the feature flag and relevant changes have already been merged, you can confirm that the tests pass on the default branch. The end-to-end tests run on the default branch every two hours, and the results are posted to a [Test Session Report, which is available in the testcase-sessions project](https://gitlab.com/gitlab-org/quality/testcase-sessions/-/issues?label_name%5B%5D=found%3Amain). diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index 4331e8e0ba8..0eb7f0645bd 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -56,7 +56,7 @@ graph TB subgraph "`gitlab-org/gitlab` pipeline" A1[`build-images` stage<br>`build-qa-image` and `build-assets-image` jobs] - A2[`qa` stage<br>`package-and-qa` job] + A2[`qa` stage<br>`e2e:package-and-test` job] end subgraph "`gitlab-org/build/omnibus-gitlab-mirror` pipeline" @@ -186,7 +186,7 @@ Use these environment variables to configure metrics export: | -------- | -------- | ----------- | | `QA_INFLUXDB_URL` | `true` | Should be set to `https://influxdb.quality.gitlab.net`. No default value. | | `QA_INFLUXDB_TOKEN` | `true` | InfluxDB write token that can be found under `Influxdb auth tokens` document in `Gitlab-QA` `1Password` vault. No default value. | -| `QA_RUN_TYPE` | `false` | Arbitrary name for test execution, like `package-and-qa`. Automatically inferred from the project name for live environment test executions. No default value. | +| `QA_RUN_TYPE` | `false` | Arbitrary name for test execution, like `package-and-test`. Automatically inferred from the project name for live environment test executions. No default value. | | `QA_EXPORT_TEST_METRICS` | `false` | Flag to enable or disable metrics export. Defaults to `true`. | ## Test reports diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md index bf71696467b..1abaf3ef323 100644 --- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md +++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md @@ -35,7 +35,7 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec | `:object_storage` | The test requires a GitLab instance to be configured to use multiple [object storage types](../../../administration/object_storage.md). Uses MinIO as the object storage server. | | `:only` | The test is only to be run in specific execution contexts. See [test execution context selection](execution_context_selection.md) for more information. | | `:orchestrated` | The GitLab instance under test may be [configured by `gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#orchestrated-tests) to be different to the default GitLab configuration, or `gitlab-qa` may launch additional services in separate Docker containers, or both. Tests tagged with `:orchestrated` are excluded when testing environments where we can't dynamically modify the GitLab configuration (for example, Staging). | -| `:packages` | The test requires a GitLab instance that has the [Package Registry](../../../administration/packages/#gitlab-package-registry-administration) enabled. | +| `:packages` | The test requires a GitLab instance that has the [Package Registry](../../../administration/packages/index.md#gitlab-package-registry-administration) enabled. | | `:product_group` | Specifies what product group the test belongs to. See [Product sections, stages, groups, and categories](https://about.gitlab.com/handbook/product/categories) for the comprehensive groups list. | | `:quarantine` | The test has been [quarantined](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantining-tests), runs in a separate job that only includes quarantined tests, and is allowed to fail. The test is skipped in its regular job so that if it fails it doesn't hold up the pipeline. Note that you can also [quarantine a test only when it runs in a specific context](execution_context_selection.md#quarantine-a-test-for-a-specific-environment). | | `:relative_url` | The test requires a GitLab instance to be installed under a [relative URL](../../../install/relative_url.md). | diff --git a/doc/update/index.md b/doc/update/index.md index e95b1e1ba50..c121c5b5d5e 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -1142,7 +1142,7 @@ for more information. When [Maintenance mode](../administration/maintenance_mode/index.md) is enabled, users cannot sign in with SSO, SAML, or LDAP. -Users who were signed in before Maintenance mode was enabled, continue to be signed in. If the administrator who enabled Maintenance mode loses their session, then they can't disable Maintenance mode via the UI. In that case, you can [disable Maintenance mode via the API or Rails console](../administration/maintenance_mode/#disable-maintenance-mode). +Users who were signed in before Maintenance mode was enabled, continue to be signed in. If the administrator who enabled Maintenance mode loses their session, then they can't disable Maintenance mode via the UI. In that case, you can [disable Maintenance mode via the API or Rails console](../administration/maintenance_mode/index.md#disable-maintenance-mode). [This bug](https://gitlab.com/gitlab-org/gitlab/-/issues/329261) was fixed in GitLab 14.5.0 and backported into 14.4.3 and 14.3.5. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 7bb3cb4f64c..e7e5fff50bc 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -190,7 +190,7 @@ Authenticating to a remote registry is not supported when [FIPS mode](../../../d > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345434) in GitLab 14.6. The `CS_DISABLE_DEPENDENCY_LIST` CI/CD variable controls whether the scan creates a -[Dependency List](../dependency_list/) +[Dependency List](../dependency_list/index.md) report. This variable is currently only supported when the `trivy` analyzer is used. The variable's default setting of `"false"` causes the scan to create the report. To disable the report, set the variable to `"true"`: @@ -230,10 +230,10 @@ container_scanning: ``` When you enable this feature, you may see [duplicate findings](../terminology/index.md#duplicate-finding) -in the [Vulnerability Report](../vulnerability_report/) -if [Dependency Scanning](../dependency_scanning/) +in the [Vulnerability Report](../vulnerability_report/index.md) +if [Dependency Scanning](../dependency_scanning/index.md) is enabled for your project. This happens because GitLab can't automatically deduplicate findings -across different types of scanning tools. Please reference [this comparison](../dependency_scanning/#dependency-scanning-compared-to-container-scanning) +across different types of scanning tools. Please reference [this comparison](../dependency_scanning/index.md#dependency-scanning-compared-to-container-scanning) between GitLab Dependency Scanning and Container Scanning for more details on which types of dependencies are likely to be duplicated. #### Available CI/CD variables @@ -649,7 +649,7 @@ you're using a non-GitLab Docker registry, you must change the `$CI_REGISTRY` va To scan an image in an external private registry, you must configure access credentials so the container scanning analyzer can authenticate itself before attempting to access the image to scan. -If you use the GitLab [Container Registry](../../packages/container_registry/), +If you use the GitLab [Container Registry](../../packages/container_registry/index.md), the `DOCKER_USER` and `DOCKER_PASSWORD` [configuration variables](#available-cicd-variables) are set automatically and you can skip this configuration. @@ -666,7 +666,7 @@ container_scanning: DOCKER_IMAGE: "gcr.io/path-to-you-registry/image:tag" ``` -Before you commit this configuration, [add a CI/CD variable](../../../ci/variables/#add-a-cicd-variable-to-a-project) +Before you commit this configuration, [add a CI/CD variable](../../../ci/variables/index.md#add-a-cicd-variable-to-a-project) for `GCP_CREDENTIALS` containing the JSON key, as described in the [Google Cloud Platform Container Registry documentation](https://cloud.google.com/container-registry/docs/advanced-authentication#json-key). Also: diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 25fd3f0e0bf..9335854133f 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -133,7 +133,7 @@ The available slash commands for Mattermost are: ## Related topics - [Mattermost slash commands](https://developers.mattermost.com/integrate/slash-commands/) -- [Omnibus GitLab Mattermost](../../../integration/mattermost/) +- [Omnibus GitLab Mattermost](../../../integration/mattermost/index.md) ## Troubleshooting diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 14cdc6877e0..aba149bb891 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -217,7 +217,7 @@ Therefore, in projects with compliance frameworks, we recommend replacing [parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md) with the following: - Direct [`include`](../../../ci/yaml/index.md#include) statements that provide the parent pipeline with child pipeline configuration. -- Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/) rather than the parent-child +- Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/index.md) rather than the parent-child pipeline feature. This alternative ensures the compliance pipeline does not re-start the parent pipeline. @@ -241,17 +241,17 @@ Use the toggles to enable or disable features in the project. | Option | More access limit options | Description | |:---------------------------------|:--------------------------|:--------------| | **Issues** | ✓ | Activates the GitLab issues tracker. | -| **Repository** | ✓ | Enables [repository](../repository/) functionality | -| **Merge requests** | ✓ | Enables [merge request](../merge_requests/) functionality; also see [Merge request settings](#configure-merge-request-settings-for-a-project). | +| **Repository** | ✓ | Enables [repository](../repository/index.md) functionality | +| **Merge requests** | ✓ | Enables [merge request](../merge_requests/index.md) functionality; also see [Merge request settings](#configure-merge-request-settings-for-a-project). | | **Forks** | ✓ | Enables [forking](../repository/forking_workflow.md) functionality. | | **Git Large File Storage (LFS)** | | Enables the use of [large files](../../../topics/git/lfs/index.md#git-large-file-storage-lfs). | | **Packages** | | Supports configuration of a [package registry](../../../administration/packages/index.md#gitlab-package-registry-administration) functionality. | | **CI/CD** | ✓ | Enables [CI/CD](../../../ci/index.md) functionality. | -| **Container Registry** | | Activates a [registry](../../packages/container_registry/) for your Docker images. | -| **Analytics** | ✓ | Enables [analytics](../../analytics/). | +| **Container Registry** | | Activates a [registry](../../packages/container_registry/index.md) for your Docker images. | +| **Analytics** | ✓ | Enables [analytics](../../analytics/index.md). | | **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md). | | **Security & Compliance** | ✓ | Control access to [security features](../../application_security/index.md). | -| **Wiki** | ✓ | Enables a separate system for [documentation](../wiki/). | +| **Wiki** | ✓ | Enables a separate system for [documentation](../wiki/index.md). | | **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md). | | **Pages** | ✓ | Allows you to [publish static websites](../pages/index.md). | | **Operations** | ✓ | Control access to Operations-related features, including [Operations Dashboard](../../../operations/index.md), [Environments and Deployments](../../../ci/environments/index.md), [Feature Flags](../../../operations/feature_flags.md). | diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 5d5bbd0bb32..8a6590fd986 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -32,7 +32,7 @@ To prevent exceeding the namespace storage quota, you can: 1. Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page. 1. Apply for [GitLab for Education](https://about.gitlab.com/solutions/education/join/), [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/), or [GitLab for Startups](https://about.gitlab.com/solutions/startups/) if you meet the eligibility requirements. -1. Consider using a [self-managed instance](../subscriptions/self_managed/) of GitLab which does not have these limits on the free tier. +1. Consider using a [self-managed instance](../subscriptions/self_managed/index.md) of GitLab which does not have these limits on the free tier. 1. [Purchase additional storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) units at $60/year for 10GB of storage. 1. [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality. 1. [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) to learn more about your options and ask questions. diff --git a/lib/gitlab/no_cache_headers.rb b/lib/gitlab/no_cache_headers.rb index f80ca2c1369..2d03741480d 100644 --- a/lib/gitlab/no_cache_headers.rb +++ b/lib/gitlab/no_cache_headers.rb @@ -4,8 +4,8 @@ module Gitlab module NoCacheHeaders DEFAULT_GITLAB_NO_CACHE_HEADERS = { 'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache", - 'Pragma' => 'no-cache', # HTTP 1.0 compatibility - 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT' + 'Pragma' => 'no-cache', # HTTP 1.0 compatibility + 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT' }.freeze def no_cache_headers diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb index ca92fed9c40..24e2eca420e 100644 --- a/lib/gitlab/sidekiq_daemon/memory_killer.rb +++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb @@ -41,11 +41,11 @@ module Gitlab def init_metrics { - sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'), + sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'), sidekiq_memory_killer_soft_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_soft_limit_rss, 'Current soft_limit_rss of Sidekiq Worker'), sidekiq_memory_killer_hard_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_hard_limit_rss, 'Current hard_limit_rss of Sidekiq Worker'), - sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'), - sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached') + sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'), + sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached') } end diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb index 180cdad916b..3dd5355d3a3 100644 --- a/lib/gitlab/sidekiq_middleware/server_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb @@ -22,21 +22,21 @@ module Gitlab def metrics { - sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS), - sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS), - sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), - sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), - sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS), + sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds this Sidekiq job spent on the CPU', {}, SIDEKIQ_LATENCY_BUCKETS), + sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_JOB_DURATION_BUCKETS), + sidekiq_jobs_db_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_db_seconds, 'Seconds of database time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), + sidekiq_jobs_gitaly_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_gitaly_seconds, 'Seconds of Gitaly time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS), + sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_QUEUE_DURATION_BUCKETS), sidekiq_redis_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_redis_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent requests a Redis server', {}, Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS), sidekiq_elasticsearch_requests_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_elasticsearch_requests_duration_seconds, 'Duration in seconds that a Sidekiq job spent in requests to an Elasticsearch server', {}, SIDEKIQ_LATENCY_BUCKETS), - sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'), - sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'), - sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'), - sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'), - sidekiq_elasticsearch_requests_total: ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_total, 'Elasticsearch requests during a Sidekiq job execution'), - sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all), - sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all), - sidekiq_mem_total_bytes: ::Gitlab::Metrics.gauge(:sidekiq_mem_total_bytes, 'Number of bytes allocated for both objects consuming an object slot and objects that required a malloc', {}, :all) + sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'), + sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'), + sidekiq_jobs_interrupted_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_interrupted_total, 'Sidekiq jobs interrupted'), + sidekiq_redis_requests_total: ::Gitlab::Metrics.counter(:sidekiq_redis_requests_total, 'Redis requests during a Sidekiq job execution'), + sidekiq_elasticsearch_requests_total: ::Gitlab::Metrics.counter(:sidekiq_elasticsearch_requests_total, 'Elasticsearch requests during a Sidekiq job execution'), + sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all), + sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all), + sidekiq_mem_total_bytes: ::Gitlab::Metrics.gauge(:sidekiq_mem_total_bytes, 'Number of bytes allocated for both objects consuming an object slot and objects that required a malloc', {}, :all) } end diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb index d28b5fb509a..55497c5e365 100644 --- a/lib/gitlab/slash_commands/presenters/base.rb +++ b/lib/gitlab/slash_commands/presenters/base.rb @@ -87,16 +87,16 @@ module Gitlab { attachments: [ { - title: "#{issue.title} · #{issue.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url(only_path: false), - fallback: fallback_message, - pretext: custom_pretext, - text: text, - color: color(resource), - fields: fields, - mrkdwn_in: fields_with_markdown + title: "#{issue.title} · #{issue.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url(only_path: false), + fallback: fallback_message, + pretext: custom_pretext, + text: text, + color: color(resource), + fields: fields, + mrkdwn_in: fields_with_markdown } ] } diff --git a/lib/gitlab/template/gitignore_template.rb b/lib/gitlab/template/gitignore_template.rb index 72a1b7460c2..d8e0ec82410 100644 --- a/lib/gitlab/template/gitignore_template.rb +++ b/lib/gitlab/template/gitignore_template.rb @@ -11,7 +11,7 @@ module Gitlab def categories { "Languages" => '', - "Global" => 'Global' + "Global" => 'Global' } end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 049e3befe64..7360585df43 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -47,17 +47,17 @@ module Gitlab def options { - s_('VisibilityLevel|Private') => PRIVATE, + s_('VisibilityLevel|Private') => PRIVATE, s_('VisibilityLevel|Internal') => INTERNAL, - s_('VisibilityLevel|Public') => PUBLIC + s_('VisibilityLevel|Public') => PUBLIC } end def string_options { - 'private' => PRIVATE, + 'private' => PRIVATE, 'internal' => INTERNAL, - 'public' => PUBLIC + 'public' => PUBLIC } end diff --git a/lib/product_analytics/event_params.rb b/lib/product_analytics/event_params.rb index 07e0bc8b43a..6cb3d462384 100644 --- a/lib/product_analytics/event_params.rb +++ b/lib/product_analytics/event_params.rb @@ -11,41 +11,41 @@ module ProductAnalytics class EventParams def self.parse_event_params(params) { - project_id: params['aid'], - platform: params['p'], - collector_tstamp: Time.zone.now, - event_id: params['eid'], - v_tracker: params['tv'], - v_collector: Gitlab::VERSION, - v_etl: Gitlab::VERSION, - os_timezone: params['tz'], - name_tracker: params['tna'], - br_lang: params['lang'], - doc_charset: params['cs'], - br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']), - br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']), - br_features_java: Gitlab::Utils.to_boolean(params['f_java']), - br_features_director: Gitlab::Utils.to_boolean(params['f_dir']), - br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']), - br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']), + project_id: params['aid'], + platform: params['p'], + collector_tstamp: Time.zone.now, + event_id: params['eid'], + v_tracker: params['tv'], + v_collector: Gitlab::VERSION, + v_etl: Gitlab::VERSION, + os_timezone: params['tz'], + name_tracker: params['tna'], + br_lang: params['lang'], + doc_charset: params['cs'], + br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']), + br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']), + br_features_java: Gitlab::Utils.to_boolean(params['f_java']), + br_features_director: Gitlab::Utils.to_boolean(params['f_dir']), + br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']), + br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']), br_features_windowsmedia: Gitlab::Utils.to_boolean(params['f_wma']), - br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']), - br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']), - br_colordepth: params['cd'], - br_cookies: Gitlab::Utils.to_boolean(params['cookie']), - dvce_created_tstamp: params['dtm'], - br_viewheight: params['vp'], - domain_sessionidx: params['vid'], - domain_sessionid: params['sid'], - domain_userid: params['duid'], - user_fingerprint: params['fp'], - page_referrer: params['refr'], - page_url: params['url'], - se_category: params['se_ca'], - se_action: params['se_ac'], - se_label: params['se_la'], - se_property: params['se_pr'], - se_value: params['se_va'] + br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']), + br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']), + br_colordepth: params['cd'], + br_cookies: Gitlab::Utils.to_boolean(params['cookie']), + dvce_created_tstamp: params['dtm'], + br_viewheight: params['vp'], + domain_sessionidx: params['vid'], + domain_sessionid: params['sid'], + domain_userid: params['duid'], + user_fingerprint: params['fp'], + page_referrer: params['refr'], + page_url: params['url'], + se_category: params['se_ca'], + se_action: params['se_ac'], + se_label: params['se_la'], + se_property: params['se_pr'], + se_value: params['se_va'] } end diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb index 1c04a7b117d..63eea0ea500 100644 --- a/lib/sidebars/projects/menus/infrastructure_menu.rb +++ b/lib/sidebars/projects/menus/infrastructure_menu.rb @@ -54,12 +54,12 @@ module Sidebars { disabled: true, data: { trigger: 'manual', - container: 'body', - placement: 'right', - highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION, - highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION], - dismiss_endpoint: callouts_path, - auto_devops_help_path: help_page_path('topics/autodevops/index.md') } } + container: 'body', + placement: 'right', + highlight: Users::CalloutsHelper::GKE_CLUSTER_INTEGRATION, + highlight_priority: Users::Callout.feature_names[:GKE_CLUSTER_INTEGRATION], + dismiss_endpoint: callouts_path, + auto_devops_help_path: help_page_path('topics/autodevops/index.md') } } end def terraform_menu_item diff --git a/lib/tasks/gitlab/import_export/export.rake b/lib/tasks/gitlab/import_export/export.rake index 4bdc62c9319..3cefdcc1aaf 100644 --- a/lib/tasks/gitlab/import_export/export.rake +++ b/lib/tasks/gitlab/import_export/export.rake @@ -27,9 +27,9 @@ namespace :gitlab do task = Gitlab::ImportExport::Project::ExportTask.new( namespace_path: args.namespace_path, - project_path: args.project_path, - username: args.username, - file_path: args.archive_path, + project_path: args.project_path, + username: args.username, + file_path: args.archive_path, logger: logger ) diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake index 2702b530334..fc727eda380 100644 --- a/lib/tasks/gitlab/import_export/import.rake +++ b/lib/tasks/gitlab/import_export/import.rake @@ -31,9 +31,9 @@ namespace :gitlab do task = Gitlab::ImportExport::Project::ImportTask.new( namespace_path: args.namespace_path, - project_path: args.project_path, - username: args.username, - file_path: args.archive_path, + project_path: args.project_path, + username: args.username, + file_path: args.archive_path, logger: logger ) diff --git a/lib/tasks/tanuki_emoji.rake b/lib/tasks/tanuki_emoji.rake index 0dc7dd4e701..b02d7a532c4 100644 --- a/lib/tasks/tanuki_emoji.rake +++ b/lib/tasks/tanuki_emoji.rake @@ -148,11 +148,11 @@ namespace :tanuki_emoji do SpriteFactory.run!(tmpdir, { output_style: style_path, output_image: "app/assets/images/emoji.png", - selector: '.emoji-', - style: :scss, - nocomments: true, - pngcrush: true, - layout: :packed + selector: '.emoji-', + style: :scss, + nocomments: true, + pngcrush: true, + layout: :packed }) # SpriteFactory's SCSS is a bit too verbose for our purposes here, so @@ -215,10 +215,10 @@ namespace :tanuki_emoji do # Combine the resized assets into a packed sprite and re-generate the SCSS SpriteFactory.run!(tmpdir, { output_image: "app/assets/images/emoji@2x.png", - style: false, - nocomments: true, - pngcrush: true, - layout: :packed + style: false, + nocomments: true, + pngcrush: true, + layout: :packed }) end diff --git a/scripts/trigger-build.rb b/scripts/trigger-build.rb index 8183a816b26..03d789e5dae 100755 --- a/scripts/trigger-build.rb +++ b/scripts/trigger-build.rb @@ -163,7 +163,7 @@ module Trigger class Omnibus < Base def self.access_token - # Default to "Multi-pipeline (from 'gitlab-org/gitlab' 'package-and-qa' job)" at https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/-/settings/access_tokens + # Default to "Multi-pipeline (from 'gitlab-org/gitlab' 'e2e:package-and-test' job)" at https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/-/settings/access_tokens ENV['OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN'] || super end diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb index 250cc3cf2cf..2e734d81ea0 100644 --- a/spec/controllers/concerns/product_analytics_tracking_spec.rb +++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb @@ -18,7 +18,7 @@ RSpec.describe ProductAnalyticsTracking, :snowplow do skip_before_action :authenticate_user!, only: :show track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow], - conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } + conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } def index render html: 'index' diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb index 178684ae2d0..0ad8fa79e5e 100644 --- a/spec/controllers/concerns/redis_tracking_spec.rb +++ b/spec/controllers/concerns/redis_tracking_spec.rb @@ -11,7 +11,8 @@ RSpec.describe RedisTracking do include RedisTracking skip_before_action :authenticate_user!, only: :show - track_redis_hll_event(:index, :show, name: 'g_compliance_approval_rules', + track_redis_hll_event(:index, :show, + name: 'g_compliance_approval_rules', if: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } def index diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index af220e2d515..e73e61b6ec5 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -49,10 +49,10 @@ RSpec.describe Import::BitbucketController do let(:expires_in) { 1.day } let(:access_token) do double(token: token, - secret: secret, - expires_at: expires_at, - expires_in: expires_in, - refresh_token: refresh_token) + secret: secret, + expires_at: expires_at, + expires_in: expires_in, + refresh_token: refresh_token) end before do diff --git a/spec/controllers/oauth/token_info_controller_spec.rb b/spec/controllers/oauth/token_info_controller_spec.rb index b66fff4d4e9..3cd952d4935 100644 --- a/spec/controllers/oauth/token_info_controller_spec.rb +++ b/spec/controllers/oauth/token_info_controller_spec.rb @@ -24,12 +24,12 @@ RSpec.describe Oauth::TokenInfoController do expect(response).to have_gitlab_http_status(:ok) expect(Gitlab::Json.parse(response.body)).to eq( - 'scope' => %w[api], - 'scopes' => %w[api], - 'created_at' => access_token.created_at.to_i, - 'expires_in' => access_token.expires_in, - 'application' => { 'uid' => application.uid }, - 'resource_owner_id' => access_token.resource_owner_id, + 'scope' => %w[api], + 'scopes' => %w[api], + 'created_at' => access_token.created_at.to_i, + 'expires_in' => access_token.expires_in, + 'application' => { 'uid' => application.uid }, + 'resource_owner_id' => access_token.resource_owner_id, 'expires_in_seconds' => access_token.expires_in ) end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 9ecef8b7450..df5da29495e 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -406,7 +406,7 @@ RSpec.describe OmniauthCallbacksController, type: :controller do before do stub_last_request_id(last_request_id) stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], - providers: [saml_config]) + providers: [saml_config]) mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index 958fcd4360c..263f488ddbf 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -488,7 +488,7 @@ RSpec.describe Projects::ArtifactsController do context 'with regular branch' do before do pipeline.update!(ref: 'master', - sha: project.commit('master').sha) + sha: project.commit('master').sha) get :latest_succeeded, params: params_from_ref('master') end @@ -499,7 +499,7 @@ RSpec.describe Projects::ArtifactsController do context 'with branch name containing slash' do before do pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) + sha: project.commit('improve/awesome').sha) get :latest_succeeded, params: params_from_ref('improve/awesome') end @@ -510,7 +510,7 @@ RSpec.describe Projects::ArtifactsController do context 'with branch name and path containing slashes' do before do pipeline.update!(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) + sha: project.commit('improve/awesome').sha) get :latest_succeeded, params: params_from_ref('improve/awesome', job.name, 'file/README.md') end diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb index d7850cc5f33..29ad51d590f 100644 --- a/spec/controllers/projects/feature_flags_controller_spec.rb +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -194,7 +194,7 @@ RSpec.describe Projects::FeatureFlagsController do other_project = create(:project) other_project.add_developer(user) other_feature_flag = create(:operations_feature_flag, project: other_project, - name: 'other_flag') + name: 'other_flag') params = { namespace_id: other_project.namespace, project_id: other_project, @@ -486,7 +486,7 @@ RSpec.describe Projects::FeatureFlagsController do context 'when creating a version 2 feature flag with a gitlabUserList strategy' do let!(:user_list) do create(:operations_feature_flag_user_list, project: project, - name: 'My List', user_xids: 'user1,user2') + name: 'My List', user_xids: 'user1,user2') end let(:params) do diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb index baee9705127..2e25b0271ce 100644 --- a/spec/controllers/projects/grafana_api_controller_spec.rb +++ b/spec/controllers/projects/grafana_api_controller_spec.rb @@ -52,8 +52,8 @@ RSpec.describe Projects::GrafanaApiController do .with(project, '1', 'api/v1/query_range', { 'query' => params[:query], 'start' => params[:start_time], - 'end' => params[:end_time], - 'step' => params[:step] }) + 'end' => params[:end_time], + 'step' => params[:step] }) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq({}) diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 77acd5fe13c..fa90a0d9abc 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -360,7 +360,7 @@ RSpec.describe Projects::PipelineSchedulesController do id: pipeline_schedule, schedule: schedule }, - as: :html + as: :html end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index c03a280d2cd..7b786f4a8af 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -167,7 +167,7 @@ RSpec.describe Projects::Registry::TagsController do repository_id: repository, ids: names }, - format: :json + format: :json end end diff --git a/spec/controllers/projects/service_desk_controller_spec.rb b/spec/controllers/projects/service_desk_controller_spec.rb index 1c4d6665414..bc507a033dc 100644 --- a/spec/controllers/projects/service_desk_controller_spec.rb +++ b/spec/controllers/projects/service_desk_controller_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::ServiceDeskController do let_it_be(:project) do create(:project, :private, :custom_repo, service_desk_enabled: true, - files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) + files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) end let_it_be(:user) { create(:user) } diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb index 5a50b3de772..0fea0c1f37f 100644 --- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb +++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb @@ -24,6 +24,76 @@ RSpec.describe 'Project > Settings > Packages & Registries > Container registry subject expect(find('.breadcrumbs')).to have_content('Clean up image tags') + + section = find('[data-testid="container-expiration-policy-project-settings"]') + expect(section).to have_text 'Clean up image tags' + end + + it 'saves cleanup policy submit the form' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + select('Every day', from: 'Run cleanup') + select('50 tags per image name', from: 'Keep the most recent:') + fill_in('Keep tags matching:', with: 'stable') + select('7 days', from: 'Remove tags older than:') + fill_in('Remove tags matching:', with: '.*-production') + + submit_button = find('[data-testid="save-button"') + expect(submit_button).not_to be_disabled + submit_button.click + end + + expect(find('.gl-toast')).to have_content('Cleanup policy successfully saved.') + end + + it 'does not save cleanup policy submit form with invalid regex' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + fill_in('Remove tags matching:', with: '*-production') + + submit_button = find('[data-testid="save-button"') + expect(submit_button).not_to be_disabled + submit_button.click + end + + expect(find('.gl-toast')).to have_content('Something went wrong while updating the cleanup policy.') + end + end + + context 'with a project without expiration policy', :js do + before do + project.container_expiration_policy.destroy! + end + + context 'with container_expiration_policies_enable_historic_entries enabled' do + before do + stub_application_setting(container_expiration_policies_enable_historic_entries: true) + end + + it 'displays the related section' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + expect(find('[data-testid="enable-toggle"]')) + .to have_content('Disabled - Tags will not be automatically deleted.') + end + end + end + + context 'with container_expiration_policies_enable_historic_entries disabled' do + before do + stub_application_setting(container_expiration_policies_enable_historic_entries: false) + end + + it 'does not display the related section' do + subject + + within '[data-testid="container-expiration-policy-project-settings"]' do + expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') + end + end end end diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js new file mode 100644 index 00000000000..51fcb1d7300 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js @@ -0,0 +1,164 @@ +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import component from '~/packages_and_registries/settings/project/components/cleanup_image_tags.vue'; +import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue'; +import { + CONTAINER_CLEANUP_POLICY_TITLE, + CONTAINER_CLEANUP_POLICY_DESCRIPTION, + FETCH_SETTINGS_ERROR_MESSAGE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + UNAVAILABLE_USER_FEATURE_TEXT, +} from '~/packages_and_registries/settings/project/constants'; +import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; + +import { + expirationPolicyPayload, + emptyExpirationPolicyPayload, + containerExpirationPolicyData, +} from '../mock_data'; + +describe('Cleanup image tags project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + isAdmin: false, + adminSettingsPath: 'settingsPath', + enableHistoricEntries: false, + helpPagePath: 'helpPagePath', + showCleanupPolicyLink: false, + }; + + const findFormComponent = () => wrapper.findComponent(ContainerExpirationPolicyForm); + const findAlert = () => wrapper.findComponent(GlAlert); + const findTitle = () => wrapper.findByTestId('title'); + const findDescription = () => wrapper.findByTestId('description'); + + const mountComponent = (provide = defaultProvidedValues, config) => { + wrapper = shallowMountExtended(component, { + stubs: { + GlSprintf, + }, + provide, + ...config, + }); + }; + + const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { + Vue.use(VueApollo); + + const requestHandlers = [[expirationPolicyQuery, resolver]]; + + fakeApollo = createMockApollo(requestHandlers); + mountComponent(provide, { + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('isEdited status', () => { + it.each` + description | apiResponse | workingCopy | result + ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} + ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} + ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} + ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} + ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} + `('$description', async ({ apiResponse, workingCopy, result }) => { + mountComponentWithApollo({ + provide: { ...defaultProvidedValues, enableHistoricEntries: true }, + resolver: jest.fn().mockResolvedValue(apiResponse), + }); + await waitForPromises(); + + findFormComponent().vm.$emit('input', workingCopy); + + await waitForPromises(); + + expect(findFormComponent().props('isEdited')).toBe(result); + }); + }); + + it('renders the setting form', async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(true); + expect(findTitle().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_TITLE); + expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION); + }); + + describe('the form is disabled', () => { + it('hides the form', () => { + mountComponent(); + + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + mountComponent(); + + const text = findAlert().text(); + expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT); + expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT); + }); + + describe('an admin is visiting the page', () => { + it('shows the admin part of the alert message', () => { + mountComponent({ ...defaultProvidedValues, isAdmin: true }); + + const sprintf = findAlert().find(GlSprintf); + expect(sprintf.text()).toBe('administration settings'); + expect(sprintf.find(GlLink).attributes('href')).toBe( + defaultProvidedValues.adminSettingsPath, + ); + }); + }); + }); + + describe('fetchSettingsError', () => { + beforeEach(async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')), + }); + await waitForPromises(); + }); + + it('hides the form', () => { + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE); + }); + }); + + describe('empty API response', () => { + it.each` + enableHistoricEntries | isShown + ${true} | ${true} + ${false} | ${false} + `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => { + mountComponentWithApollo({ + provide: { + ...defaultProvidedValues, + enableHistoricEntries, + }, + resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(isShown); + }); + }); +}); diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index 4ac6eaebaa2..2982dc5c46c 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -320,7 +320,9 @@ describe('Release edit/new getters', () => { it(description, () => { const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) }; - const actualVariables = getters.releaseUpdateMutatationVariables(state); + const actualVariables = getters.releaseUpdateMutatationVariables(state, { + releasedAtChanged: Object.hasOwn(state.release, 'releasedAt'), + }); expect(actualVariables).toEqual(expectedVariablesObject); }); @@ -409,4 +411,19 @@ describe('Release edit/new getters', () => { }, ); }); + + describe('releasedAtChange', () => { + it('is false if the released at date has not changed', () => { + const date = new Date(); + expect( + getters.releasedAtChanged({ originalReleasedAt: date, release: { releasedAt: date } }), + ).toBe(false); + }); + + it('is true if the date changed', () => { + const originalReleasedAt = new Date(); + const releasedAt = new Date(2022, 5, 30); + expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true); + }); + }); }); diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 60b57c7a7ff..8bbf550b77d 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -36,6 +36,12 @@ describe('Release edit/new mutations', () => { }, }); }); + + it('saves the original released at date as well', () => { + mutations[types.INITIALIZE_EMPTY_RELEASE](state); + + expect(state.originalReleasedAt).toEqual(new Date()); + }); }); describe(`${types.REQUEST_RELEASE}`, () => { @@ -57,6 +63,7 @@ describe('Release edit/new mutations', () => { expect(state.release).toEqual(release); expect(state.originalRelease).toEqual(release); + expect(state.originalReleasedAt).toEqual(release.releasedAt); }); }); diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 95982db3620..51467656908 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gi import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; @@ -12,7 +13,6 @@ import WorkItemState from '~/work_items/components/work_item_state.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; -import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; import WorkItemInformation from '~/work_items/components/work_item_information.vue'; import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; @@ -25,6 +25,7 @@ import { workItemTitleSubscriptionResponse, workItemResponseFactory, mockParent, + workItemWeightSubscriptionResponse, } from '../mock_data'; describe('WorkItemDetail component', () => { @@ -41,6 +42,7 @@ describe('WorkItemDetail component', () => { }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); const findAlert = () => wrapper.findComponent(GlAlert); const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader); @@ -51,7 +53,6 @@ describe('WorkItemDetail component', () => { const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription); const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); - const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight); const findParent = () => wrapper.find('[data-testid="work-item-parent"]'); const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); @@ -70,13 +71,19 @@ describe('WorkItemDetail component', () => { includeWidgets = false, error = undefined, } = {}) => { + const handlers = [ + [workItemQuery, handler], + [workItemTitleSubscription, subscriptionHandler], + confidentialityMock, + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = shallowMount(WorkItemDetail, { apolloProvider: createMockApollo( - [ - [workItemQuery, handler], - [workItemTitleSubscription, subscriptionHandler], - confidentialityMock, - ], + handlers, {}, { typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {}, @@ -93,6 +100,7 @@ describe('WorkItemDetail component', () => { glFeatures: { workItemsMvc2: workItemsMvc2Enabled, }, + hasIssueWeightsFeature: true, }, }); }; @@ -438,34 +446,6 @@ describe('WorkItemDetail component', () => { }); }); - describe('weight widget', () => { - describe.each` - description | weightWidgetPresent | exists - ${'when widget is returned from API'} | ${true} | ${true} - ${'when widget is not returned from API'} | ${false} | ${false} - `('$description', ({ weightWidgetPresent, exists }) => { - it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => { - const response = workItemResponseFactory({ weightWidgetPresent }); - const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); - await waitForPromises(); - - expect(findWorkItemWeight().exists()).toBe(exists); - }); - }); - - it('shows an error message when it emits an `error` event', async () => { - createComponent({ workItemsMvc2Enabled: true }); - await waitForPromises(); - const updateError = 'Failed to update'; - - findWorkItemWeight().vm.$emit('error', updateError); - await waitForPromises(); - - expect(findAlert().text()).toBe(updateError); - }); - }); - describe('work item information', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js deleted file mode 100644 index 38dc1af0bac..00000000000 --- a/spec/frontend/work_items/components/work_item_weight_spec.js +++ /dev/null @@ -1,218 +0,0 @@ -import { GlForm, GlFormInput } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockTracking } from 'helpers/tracking_helper'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { __ } from '~/locale'; -import WorkItemWeight from '~/work_items/components/work_item_weight.vue'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; -import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data'; - -describe('WorkItemWeight component', () => { - Vue.use(VueApollo); - - let wrapper; - - const workItemId = 'gid://gitlab/WorkItem/1'; - const workItemType = 'Task'; - - const findForm = () => wrapper.findComponent(GlForm); - const findInput = () => wrapper.findComponent(GlFormInput); - - const createComponent = ({ - canUpdate = false, - hasIssueWeightsFeature = true, - isEditing = false, - weight, - mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse), - } = {}) => { - wrapper = mountExtended(WorkItemWeight, { - apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), - propsData: { - canUpdate, - weight, - workItemId, - workItemType, - }, - provide: { - hasIssueWeightsFeature, - }, - }); - - if (isEditing) { - findInput().vm.$emit('focus'); - } - }; - - describe('`issue_weights` licensed feature', () => { - describe.each` - description | hasIssueWeightsFeature | exists - ${'when available'} | ${true} | ${true} - ${'when not available'} | ${false} | ${false} - `('$description', ({ hasIssueWeightsFeature, exists }) => { - it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => { - createComponent({ hasIssueWeightsFeature }); - - expect(findForm().exists()).toBe(exists); - }); - }); - }); - - describe('weight input', () => { - it('has "Weight" label', () => { - createComponent(); - - expect(wrapper.findByLabelText(__('Weight')).exists()).toBe(true); - }); - - describe('placeholder attribute', () => { - describe.each` - description | isEditing | canUpdate | value - ${'when not editing and cannot update'} | ${false} | ${false} | ${__('None')} - ${'when editing and cannot update'} | ${true} | ${false} | ${__('None')} - ${'when not editing and can update'} | ${false} | ${true} | ${__('None')} - ${'when editing and can update'} | ${true} | ${true} | ${__('Enter a number')} - `('$description', ({ isEditing, canUpdate, value }) => { - it(`has a value of "${value}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('placeholder')).toBe(value); - }); - }); - }); - - describe('readonly attribute', () => { - describe.each` - description | canUpdate | value - ${'when cannot update'} | ${false} | ${'readonly'} - ${'when can update'} | ${true} | ${undefined} - `('$description', ({ canUpdate, value }) => { - it(`renders readonly=${value}`, () => { - createComponent({ canUpdate }); - - expect(findInput().attributes('readonly')).toBe(value); - }); - }); - }); - - describe('type attribute', () => { - describe.each` - description | isEditing | canUpdate | type - ${'when not editing and cannot update'} | ${false} | ${false} | ${'text'} - ${'when editing and cannot update'} | ${true} | ${false} | ${'text'} - ${'when not editing and can update'} | ${false} | ${true} | ${'text'} - ${'when editing and can update'} | ${true} | ${true} | ${'number'} - `('$description', ({ isEditing, canUpdate, type }) => { - it(`has a value of "${type}"`, async () => { - createComponent({ canUpdate, isEditing }); - await nextTick(); - - expect(findInput().attributes('type')).toBe(type); - }); - }); - }); - - describe('value attribute', () => { - describe.each` - weight | value - ${1} | ${'1'} - ${0} | ${'0'} - ${null} | ${''} - ${undefined} | ${''} - `('when `weight` prop is "$weight"', ({ weight, value }) => { - it(`value is "${value}"`, () => { - createComponent({ weight }); - - expect(findInput().element.value).toBe(value); - }); - }); - }); - - describe('when blurred', () => { - it('calls a mutation to update the weight when the input value is different', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ - isEditing: true, - weight: 0, - mutationHandler: mutationSpy, - canUpdate: true, - }); - - findInput().vm.$emit('blur', { target: { value: 1 } }); - - expect(mutationSpy).toHaveBeenCalledWith({ - input: { - id: workItemId, - weightWidget: { - weight: 1, - }, - }, - }); - }); - - it('does not call a mutation to update the weight when the input value is the same', () => { - const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true }); - - findInput().trigger('blur'); - - expect(mutationSpy).not.toHaveBeenCalledWith(); - }); - - it('emits an error when there is a GraphQL error', async () => { - const response = { - data: { - workItemUpdate: { - errors: ['Error!'], - workItem: {}, - }, - }, - }; - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockResolvedValue(response), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([ - ['Something went wrong while updating the task. Please try again.'], - ]); - }); - - it('emits an error when there is a network error', async () => { - createComponent({ - isEditing: true, - mutationHandler: jest.fn().mockRejectedValue(new Error()), - canUpdate: true, - }); - - findInput().trigger('blur'); - await waitForPromises(); - - expect(wrapper.emitted('error')).toEqual([ - ['Something went wrong while updating the task. Please try again.'], - ]); - }); - - it('tracks updating the weight', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - createComponent({ canUpdate: true }); - - findInput().trigger('blur'); - - expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_weight', { - category: TRACKING_CATEGORY_SHOW, - label: 'item_weight', - property: 'type_Task', - }); - }); - }); - }); -}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 8deca5c50b7..6a5aa48b610 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -407,6 +407,20 @@ export const workItemTitleSubscriptionResponse = { }, }; +export const workItemWeightSubscriptionResponse = { + data: { + issuableWeightUpdated: { + id: 'gid://gitlab/WorkItem/1', + widgets: [ + { + __typename: 'WorkItemWidgetWeight', + weight: 1, + }, + ], + }, + }, +}; + export const workItemHierarchyEmptyResponse = { data: { workItem: { diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 99dcd886f7b..a86663cbdf9 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -1,5 +1,16 @@ import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { + workItemResponseFactory, + workItemTitleSubscriptionResponse, + workItemWeightSubscriptionResponse, +} from 'jest/work_items/mock_data'; import App from '~/work_items/components/app.vue'; +import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; +import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import { createRouter } from '~/work_items/router'; @@ -7,26 +18,34 @@ import { createRouter } from '~/work_items/router'; describe('Work items router', () => { let wrapper; + Vue.use(VueApollo); + + const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); + const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); + const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse); + const createComponent = async (routeArg) => { const router = createRouter('/work_item'); if (routeArg !== undefined) { await router.push(routeArg); } + const handlers = [ + [workItemQuery, workItemQueryHandler], + [workItemTitleSubscription, titleSubscriptionHandler], + ]; + + if (IS_EE) { + handlers.push([workItemWeightSubscription, weightSubscriptionHandler]); + } + wrapper = mount(App, { + apolloProvider: createMockApollo(handlers), router, provide: { fullPath: 'full-path', issuesListPath: 'full-path/-/issues', }, - mocks: { - $apollo: { - queries: { - workItem: {}, - workItemTypes: {}, - }, - }, - }, }); }; diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb index a41ade2950a..12e865b1312 100644 --- a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb +++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'a synthetic note' do |action| - it_behaves_like 'a system note', exclude_project: true do + it_behaves_like 'a system note', exclude_project: true, skip_persistence_check: true do let(:action) { action } end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index ce412ef55de..1887b38b50e 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -42,6 +42,7 @@ RSpec.shared_examples 'a system note' do |params| it 'has the correct attributes', :aggregate_failures do exclude_project = !params.nil? && params[:exclude_project] + skip_persistence_check = !params.nil? && params[:skip_persistence_check] expect(subject).to be_valid expect(subject).to be_system @@ -50,6 +51,7 @@ RSpec.shared_examples 'a system note' do |params| expect(subject.project).to eq project unless exclude_project expect(subject.author).to eq author + expect(subject.system_note_metadata).to be_persisted unless skip_persistence_check expect(subject.system_note_metadata.action).to eq(action) expect(subject.system_note_metadata.commit_count).to eq(commit_count) end |