diff options
41 files changed, 791 insertions, 210 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b551967cb3..1531b1d6b96 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,6 +69,9 @@ variables: GET_SOURCES_ATTEMPTS: "3" DEBIAN_VERSION: "bullseye" + TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests" + GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse" + TMP_TEST_GITLAB_WORKHORSE_PATH: "${TMP_TEST_FOLDER}/${GITLAB_WORKHORSE_FOLDER}" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json FLAKY_RSPEC_SUITE_REPORT_PATH: rspec/flaky/report-suite.json RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 79f3ddff3ec..abdacad93b5 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -183,7 +183,7 @@ # rspec job parallel configs ############################ -####################################################### +############################################################### # EE/FOSS: default refs (MRs, default branch, schedules) jobs # setup-test-env: extends: @@ -194,40 +194,53 @@ setup-test-env: variables: SETUP_DB: "false" script: + - source scripts/gitlab_workhorse_component_helpers.sh + - run_timed_command "download_and_extract_gitlab_workhorse_package" || true - run_timed_command "scripts/setup-test-env" + - run_timed_command "select_gitlab_workhorse_essentials" - echo -e "\e[0Ksection_start:`date +%s`:gitaly-test-build[collapsed=true]\r\e[0KCompiling Gitaly binaries" - run_timed_command "scripts/gitaly-test-build" # Do not use 'bundle exec' here - echo -e "\e[0Ksection_end:`date +%s`:gitaly-test-build\r\e[0K" - artifacts: expire_in: 7d paths: - config/secrets.yml - - tmp/tests/gitaly/_build/bin/ - - tmp/tests/gitaly/_build/deps/git/install - - tmp/tests/gitaly/config.toml - - tmp/tests/gitaly/gitaly2.config.toml - - tmp/tests/gitaly/internal/ - - tmp/tests/gitaly/internal_gitaly2/ - - tmp/tests/gitaly/internal_sockets/ - - tmp/tests/gitaly/Makefile - - tmp/tests/gitaly/praefect.config.toml - - tmp/tests/gitaly/ruby/ - - tmp/tests/gitlab-elasticsearch-indexer/bin/gitlab-elasticsearch-indexer - - tmp/tests/gitlab-shell/ - - tmp/tests/gitlab-test-fork/ - - tmp/tests/gitlab-test-fork_bare/ - - tmp/tests/gitlab-test/ - - tmp/tests/gitlab-workhorse/gitlab-zip-metadata - - tmp/tests/gitlab-workhorse/gitlab-zip-cat - - tmp/tests/gitlab-workhorse/gitlab-workhorse - - tmp/tests/gitlab-workhorse/gitlab-resize-image - - tmp/tests/gitlab-workhorse/config.toml - - tmp/tests/gitlab-workhorse/WORKHORSE_TREE - - tmp/tests/repositories/ - - tmp/tests/second_storage/ + - ${TMP_TEST_FOLDER}/gitaly/_build/bin/ + - ${TMP_TEST_FOLDER}/gitaly/_build/deps/git/install + - ${TMP_TEST_FOLDER}/gitaly/config.toml + - ${TMP_TEST_FOLDER}/gitaly/gitaly2.config.toml + - ${TMP_TEST_FOLDER}/gitaly/internal/ + - ${TMP_TEST_FOLDER}/gitaly/internal_gitaly2/ + - ${TMP_TEST_FOLDER}/gitaly/internal_sockets/ + - ${TMP_TEST_FOLDER}/gitaly/Makefile + - ${TMP_TEST_FOLDER}/gitaly/praefect.config.toml + - ${TMP_TEST_FOLDER}/gitaly/ruby/ + - ${TMP_TEST_FOLDER}/gitlab-elasticsearch-indexer/bin/gitlab-elasticsearch-indexer + - ${TMP_TEST_FOLDER}/gitlab-shell/ + - ${TMP_TEST_FOLDER}/gitlab-test-fork/ + - ${TMP_TEST_FOLDER}/gitlab-test-fork_bare/ + - ${TMP_TEST_FOLDER}/gitlab-test/ + - ${TMP_TEST_FOLDER}/repositories/ + - ${TMP_TEST_FOLDER}/second_storage/ + - ${TMP_TEST_GITLAB_WORKHORSE_PATH}/ when: always +build-components: + extends: + - setup-test-env + - .rails:rules:build-components + script: + - source scripts/gitlab_workhorse_component_helpers.sh + - 'gitlab_workhorse_package_doesnt_exist || { echoinfo "INFO: Exiting early as package exists."; exit 0; }' + - run_timed_command "scripts/setup-test-env" + - run_timed_command "select_gitlab_workhorse_essentials" + - run_timed_command "create_gitlab_workhorse_package" + - run_timed_command "upload_gitlab_workhorse_package" + artifacts: + expire_in: 7d + paths: + - ${TMP_TEST_GITLAB_WORKHORSE_PATH}/ + update-setup-test-env-cache: extends: - setup-test-env diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index a4bb99c49ad..013dda2169d 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -876,6 +876,16 @@ ############### # Rails rules # ############### +.rails:rules:build-components: + rules: + - <<: *if-dot-com-ee-schedule + - <<: *if-dot-com-gitlab-org-default-branch + changes: + - "workhorse/**/*" + - <<: *if-dot-com-gitlab-org-merge-request + when: manual + allow_failure: true + .rails:rules:setup-test-env: rules: - changes: *setup-test-env-patterns diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index f5c71f9691f..c9af5d9b4a7 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -77,9 +77,7 @@ export function initIssueApp(issueData, store) { const { fullPath } = el.dataset; - if (gon?.features?.fixCommentScroll) { - scrollToTargetOnResize(); - } + scrollToTargetOnResize(); bootstrapApollo({ ...issueState, issueType: el.dataset.issueType }); diff --git a/app/assets/javascripts/lib/utils/ignore_while_pending.js b/app/assets/javascripts/lib/utils/ignore_while_pending.js new file mode 100644 index 00000000000..e85a573c8f2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/ignore_while_pending.js @@ -0,0 +1,26 @@ +/** + * This will wrap the given function to make sure that it is only triggered once + * while executing asynchronously + * + * @param {Function} fn some function that returns a promise + * @returns A function that will only be triggered *once* while the promise is executing + */ +export const ignoreWhilePending = (fn) => { + const isPendingMap = new WeakMap(); + const defaultContext = {}; + + // We need this to be a function so we get the `this` + return function ignoreWhilePendingInner(...args) { + const context = this || defaultContext; + + if (isPendingMap.get(context)) { + return Promise.resolve(); + } + + isPendingMap.set(context, true); + + return fn.apply(this, args).finally(() => { + isPendingMap.delete(context); + }); + }; +}; diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index ddf72587ba3..c4602363da1 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -6,6 +6,7 @@ import createFlash from '~/flash'; import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending'; import { s__, __ } from '~/locale'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -171,7 +172,7 @@ export default { this.expandDiscussion({ discussionId: this.discussion.id }); } }, - async cancelReplyForm(shouldConfirm, isDirty) { + cancelReplyForm: ignoreWhilePending(async function cancelReplyForm(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); @@ -188,7 +189,7 @@ export default { this.isReplying = false; clearDraft(this.autosaveKey); - }, + }), saveReply(noteText, form, callback) { if (!noteText) { this.cancelReplyForm(); diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 7bad10616cc..a271ac91f6e 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -7,6 +7,7 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; import createFlash from '~/flash'; import httpStatusCodes from '~/lib/utils/http_status'; +import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import { __, s__, sprintf } from '../../locale'; @@ -350,7 +351,10 @@ export default { parent: this.$el, }); }, - async formCancelHandler({ shouldConfirm, isDirty }) { + formCancelHandler: ignoreWhilePending(async function formCancelHandler({ + shouldConfirm, + isDirty, + }) { if (shouldConfirm && isDirty) { const msg = __('Are you sure you want to cancel editing this comment?'); const confirmed = await confirmAction(msg); @@ -364,7 +368,7 @@ export default { } this.isEditing = false; this.$emit('cancelForm'); - }, + }), recoverNoteContent(noteText) { // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content diff --git a/app/assets/javascripts/pipeline_wizard/components/input.vue b/app/assets/javascripts/pipeline_wizard/components/input.vue new file mode 100644 index 00000000000..9a0c8026648 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/input.vue @@ -0,0 +1,99 @@ +<script> +import { isNode, isDocument, isSeq, visit } from 'yaml'; +import { capitalize } from 'lodash'; +import TextWidget from '~/pipeline_wizard/components/widgets/text.vue'; +import ListWidget from '~/pipeline_wizard/components/widgets/list.vue'; + +const widgets = { + TextWidget, + ListWidget, +}; + +function isNullOrUndefined(v) { + return [undefined, null].includes(v); +} + +export default { + components: { + ...widgets, + }, + props: { + template: { + type: Object, + required: true, + validator: (v) => isNode(v), + }, + compiled: { + type: Object, + required: true, + validator: (v) => isDocument(v) || isNode(v), + }, + target: { + type: String, + required: true, + validator: (v) => /^\$.*/g.test(v), + }, + widget: { + type: String, + required: true, + validator: (v) => { + return Object.keys(widgets).includes(`${capitalize(v)}Widget`); + }, + }, + validate: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + path() { + let res; + visit(this.template, (seqKey, node, path) => { + if (node && node.value === this.target) { + // `path` is an array of objects (all the node's parents) + // So this reducer will reduce it to an array of the path's keys, + // e.g. `[ 'foo', 'bar', '0' ]` + res = path.reduce((p, { key }) => (key ? [...p, `${key}`] : p), []); + const parent = path[path.length - 1]; + if (isSeq(parent)) { + res.push(seqKey); + } + } + }); + return res; + }, + }, + methods: { + compile(v) { + if (!this.path) return; + if (isNullOrUndefined(v)) { + this.compiled.deleteIn(this.path); + } + this.compiled.setIn(this.path, v); + }, + onModelChange(v) { + this.$emit('beforeUpdate:compiled'); + this.compile(v); + this.$emit('update:compiled', this.compiled); + this.$emit('highlight', this.path); + }, + onValidationStateChange(v) { + this.$emit('update:valid', v); + }, + }, +}; +</script> + +<template> + <div> + <component + :is="`${widget}-widget`" + ref="widget" + :validate="validate" + v-bind="$attrs" + @input="onModelChange" + @update:valid="onValidationStateChange" + /> + </div> +</template> diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5d0a1c3b978..c8b1ed04e4a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -49,7 +49,6 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml) - push_frontend_feature_flag(:fix_comment_scroll, project, default_enabled: :yaml) push_frontend_feature_flag(:work_items, project&.group, default_enabled: :yaml) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 9973cf3fe8d..1eb30e88f16 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -74,6 +74,7 @@ module Issuable end has_many :note_authors, -> { distinct }, through: :notes, source: :author + has_many :user_note_authors, -> { distinct.where("notes.system = false") }, through: :notes, source: :author has_many :label_links, as: :target, inverse_of: :target has_many :labels, through: :label_links diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 894e60b61f2..22dd75fc64d 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -362,3 +362,9 @@ - - :approve - 0.0.62 - *2 +- - :approve + - argparse + - :who: Lukas Eipert + :why: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79864#note_845406018 + :versions: [2.0.1] + :when: 2022-02-24 10:44:26.669326000 Z diff --git a/config/feature_flags/development/fix_comment_scroll.yml b/config/feature_flags/development/fix_comment_scroll.yml deleted file mode 100644 index 706cd816288..00000000000 --- a/config/feature_flags/development/fix_comment_scroll.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: fix_comment_scroll -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76340 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349638 -milestone: '14.7' -type: development -group: group::project management -default_enabled: false diff --git a/db/migrate/20211021115409_add_color_to_epics.rb b/db/migrate/20211021115409_add_color_to_epics.rb new file mode 100644 index 00000000000..14b38209f30 --- /dev/null +++ b/db/migrate/20211021115409_add_color_to_epics.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddColorToEpics < Gitlab::Database::Migration[1.0] + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20211021124715_add_text_limit_to_epics_color + def change + add_column :epics, :color, :text, default: '#1068bf' + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20211021124715_add_text_limit_to_epics_color.rb b/db/migrate/20211021124715_add_text_limit_to_epics_color.rb new file mode 100644 index 00000000000..7844575c521 --- /dev/null +++ b/db/migrate/20211021124715_add_text_limit_to_epics_color.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitToEpicsColor < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + add_text_limit :epics, :color, 7 + end + + def down + remove_text_limit :epics, :color + end +end diff --git a/db/schema_migrations/20211021115409 b/db/schema_migrations/20211021115409 new file mode 100644 index 00000000000..bcbed298377 --- /dev/null +++ b/db/schema_migrations/20211021115409 @@ -0,0 +1 @@ +93960203e6703716f9c513dca340e17041a33792f9233dc4b7e35d1e19614191
\ No newline at end of file diff --git a/db/schema_migrations/20211021124715 b/db/schema_migrations/20211021124715 new file mode 100644 index 00000000000..2d03c608bac --- /dev/null +++ b/db/schema_migrations/20211021124715 @@ -0,0 +1 @@ +406af18458c7f5ee8a4fa3860ed5fb87c358363926fed2830be8e8a55578822b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 111cb4ca521..d402eeb51e2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14722,6 +14722,8 @@ CREATE TABLE epics ( due_date_sourcing_epic_id integer, confidential boolean DEFAULT false NOT NULL, external_key character varying(255), + color text DEFAULT '#1068bf'::text, + CONSTRAINT check_ca608c40b3 CHECK ((char_length(color) <= 7)), CONSTRAINT check_fcfb4a93ff CHECK ((lock_version IS NOT NULL)) ); diff --git a/doc/api/epics.md b/doc/api/epics.md index deb74cf21e9..de20af08915 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -131,6 +131,7 @@ Example response: "labels": [], "upvotes": 4, "downvotes": 0, + "color": "#1068bf", "_links":{ "self": "http://gitlab.example.com/api/v4/groups/7/epics/4", "epic_issues": "http://gitlab.example.com/api/v4/groups/7/epics/4/issues", @@ -179,6 +180,7 @@ Example response: "labels": [], "upvotes": 4, "downvotes": 0, + "color": "#1068bf", "_links":{ "self": "http://gitlab.example.com/api/v4/groups/17/epics/35", "epic_issues": "http://gitlab.example.com/api/v4/groups/17/epics/35/issues", @@ -252,6 +254,7 @@ Example response: "labels": [], "upvotes": 4, "downvotes": 0, + "color": "#1068bf", "subscribed": true, "_links":{ "self": "http://gitlab.example.com/api/v4/groups/7/epics/5", @@ -283,6 +286,7 @@ POST /groups/:id/epics | `title` | string | yes | The title of the epic | | `labels` | string | no | The comma-separated list of labels | | `description` | string | no | The description of the epic. Limited to 1,048,576 characters. | +| `color` | string | no | The color of the epic. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7641) in GitLab 14.8, behind a feature flag named `epic_highlight_color` (disabled by default) | | `confidential` | boolean | no | Whether the epic should be confidential | | `created_at` | string | no | When the epic was created. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([available](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5 and later) | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (in GitLab 11.3 and later) | @@ -340,6 +344,7 @@ Example response: "labels": [], "upvotes": 4, "downvotes": 0, + "color": "#1068bf", "_links":{ "self": "http://gitlab.example.com/api/v4/groups/7/epics/6", "epic_issues": "http://gitlab.example.com/api/v4/groups/7/epics/6/issues", @@ -381,6 +386,7 @@ PUT /groups/:id/epics/:epic_iid | `state_event` | string | no | State event for an epic. Set `close` to close the epic and `reopen` to reopen it (in GitLab 11.4 and later) | | `title` | string | no | The title of an epic | | `updated_at` | string | no | When the epic was updated. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([available](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5 and later) | +| `color` | string | no | The color of the epic. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7641) in GitLab 14.8, behind a feature flag named `epic_highlight_color` (disabled by default) | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title&parent_id=29" @@ -430,7 +436,8 @@ Example response: "closed_at": "2018-08-18T12:22:05.239Z", "labels": [], "upvotes": 4, - "downvotes": 0 + "downvotes": 0, + "color": "#1068bf" } ``` diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 824457ccda8..aa6c4da6262 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1366,6 +1366,7 @@ Input type: `CreateEpicInput` | ---- | ---- | ----------- | | <a id="mutationcreateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. | | <a id="mutationcreateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationcreateepiccolor"></a>`color` | [`String`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="mutationcreateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. | | <a id="mutationcreateepicdescription"></a>`description` | [`String`](#string) | Description of the epic. | | <a id="mutationcreateepicduedatefixed"></a>`dueDateFixed` | [`String`](#string) | End date of the epic. | @@ -4756,6 +4757,7 @@ Input type: `UpdateEpicInput` | ---- | ---- | ----------- | | <a id="mutationupdateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. | | <a id="mutationupdateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationupdateepiccolor"></a>`color` | [`String`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="mutationupdateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. | | <a id="mutationupdateepicdescription"></a>`description` | [`String`](#string) | Description of the epic. | | <a id="mutationupdateepicduedatefixed"></a>`dueDateFixed` | [`String`](#string) | End date of the epic. | @@ -8873,6 +8875,7 @@ Represents an epic on an issue board. | <a id="boardepicauthor"></a>`author` | [`UserCore!`](#usercore) | Author of the epic. | | <a id="boardepicawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of award emojis associated with the epic. (see [Connections](#connections)) | | <a id="boardepicclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the epic was closed. | +| <a id="boardepiccolor"></a>`color` | [`String!`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="boardepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. | | <a id="boardepiccreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of when the epic was created. | | <a id="boardepicdescendantcounts"></a>`descendantCounts` | [`EpicDescendantCount`](#epicdescendantcount) | Number of open and closed descendant epics and issues. | @@ -8908,6 +8911,7 @@ Represents an epic on an issue board. | <a id="boardepicstartdateisfixed"></a>`startDateIsFixed` | [`Boolean`](#boolean) | Indicates if the start date has been manually set. | | <a id="boardepicstate"></a>`state` | [`EpicState!`](#epicstate) | State of the epic. | | <a id="boardepicsubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the epic. | +| <a id="boardepictextcolor"></a>`textColor` | [`String!`](#string) | Text color generated for the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="boardepictitle"></a>`title` | [`String`](#string) | Title of the epic. | | <a id="boardepictitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | | <a id="boardepicupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the epic was updated. | @@ -10415,6 +10419,7 @@ Represents an epic. | <a id="epicauthor"></a>`author` | [`UserCore!`](#usercore) | Author of the epic. | | <a id="epicawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of award emojis associated with the epic. (see [Connections](#connections)) | | <a id="epicclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the epic was closed. | +| <a id="epiccolor"></a>`color` | [`String!`](#string) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="epicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. | | <a id="epiccreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of when the epic was created. | | <a id="epicdescendantcounts"></a>`descendantCounts` | [`EpicDescendantCount`](#epicdescendantcount) | Number of open and closed descendant epics and issues. | @@ -10450,6 +10455,7 @@ Represents an epic. | <a id="epicstartdateisfixed"></a>`startDateIsFixed` | [`Boolean`](#boolean) | Indicates if the start date has been manually set. | | <a id="epicstate"></a>`state` | [`EpicState!`](#epicstate) | State of the epic. | | <a id="epicsubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the epic. | +| <a id="epictextcolor"></a>`textColor` | [`String!`](#string) | Text color generated for the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="epictitle"></a>`title` | [`String`](#string) | Title of the epic. | | <a id="epictitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | | <a id="epicupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the epic was updated. | diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 3e9c0177d48..3fc4c8c43e3 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1130,6 +1130,17 @@ copy of `https://gitlab.com/gitlab-org/gitlab`, run in a terminal: bin/pngquant compress doc/user/img ``` +### Animated images + +Sometimes an image with animation (such as an animated GIF) +can help the reader understand a complicated interaction with the user interface. + +However, you should use them sparingly and avoid them when you can. +Do not use them to replace written descriptions of processes or the product. + +If you include an animated image, follow the same size and naming conventions we use for images. If the animated image loops, add at least a three +second pause to the end of the loop. + ## Videos Adding GitLab YouTube video tutorials to the documentation is highly diff --git a/doc/user/analytics/img/mr_throughput_chart_v13_3.png b/doc/user/analytics/img/mr_throughput_chart_v13_3.png Binary files differdeleted file mode 100644 index 100c9a8557c..00000000000 --- a/doc/user/analytics/img/mr_throughput_chart_v13_3.png +++ /dev/null diff --git a/doc/user/analytics/img/project_vsa_filter_v14_3.png b/doc/user/analytics/img/project_vsa_filter_v14_3.png Binary files differdeleted file mode 100644 index d3fcad31909..00000000000 --- a/doc/user/analytics/img/project_vsa_filter_v14_3.png +++ /dev/null diff --git a/doc/user/analytics/img/project_vsa_stage_table_v14_4.png b/doc/user/analytics/img/project_vsa_stage_table_v14_4.png Binary files differdeleted file mode 100644 index e65296062f6..00000000000 --- a/doc/user/analytics/img/project_vsa_stage_table_v14_4.png +++ /dev/null diff --git a/doc/user/clusters/img/kubernetes-agent-ui-list_v14_8.png b/doc/user/clusters/img/kubernetes-agent-ui-list_v14_8.png Binary files differdeleted file mode 100644 index 5b5ba3a9804..00000000000 --- a/doc/user/clusters/img/kubernetes-agent-ui-list_v14_8.png +++ /dev/null diff --git a/doc/user/group/import/img/import_panel_v14_1.png b/doc/user/group/import/img/import_panel_v14_1.png Binary files differdeleted file mode 100644 index 52791a82c3c..00000000000 --- a/doc/user/group/import/img/import_panel_v14_1.png +++ /dev/null diff --git a/doc/user/group/import/img/new_group_navigation_v13_8.png b/doc/user/group/import/img/new_group_navigation_v13_8.png Binary files differdeleted file mode 100644 index 40be3dd41d2..00000000000 --- a/doc/user/group/import/img/new_group_navigation_v13_8.png +++ /dev/null diff --git a/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md b/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md index 881a96fc986..0cff98fb324 100644 --- a/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md +++ b/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md @@ -4,85 +4,78 @@ group: Configure info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrate to the GitLab Agent for Kubernetes **(FREE)** +# Migrate to the GitLab agent for Kubernetes **(FREE)** -The first integration between GitLab and Kubernetes used cluster certificates -to connect the cluster to GitLab. -This method was [deprecated](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/) -in GitLab 14.5 in favor of the [GitLab Agent for Kubernetes](../../clusters/agent/index.md). +To connect your Kubernetes cluster with GitLab, you can use: -To make sure your clusters connected to GitLab do not break in the future, -we recommend you migrate to the GitLab Agent as soon as possible by following -the processes described in this document. +- [A GitOps workflow](../../clusters/agent/gitops.md). +- [A GitLab CI/CD workflow](../../clusters/agent/ci_cd_tunnel.md). +- [A certificate-based integration](index.md). -The certificate-based integration was used for some popular GitLab features such as, -GitLab Managed Apps, GitLab-managed clusters, and Auto DevOps. +The certificate-based integration is +[**deprecated**](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/) +in GitLab 14.5. It is expected to be +[turned off by default in 15.0](../../../update/deprecations.md#certificate-based-integration-with-kubernetes) +and removed in GitLab 15.6. + +If you are using the certificate-based integration, you should move to another workflow as soon as possible. -As a general rule, migrating clusters that rely on GitLab CI/CD can be -achieved using the [CI/CD Tunnel](../../clusters/agent/ci_cd_tunnel.md) -provided by the Agent. +As a general rule, to migrate clusters that rely on GitLab CI/CD, +you can use the [CI/CD workflow](../../clusters/agent/ci_cd_tunnel.md). +This workflow uses an agent to connect to your cluster. The agent: + +- Is not exposed to the internet. +- Does not require full cluster-admin access to GitLab. NOTE: -The GitLab Agent for Kubernetes does not intend to provide feature parity with the -certificate-based cluster integrations. As a result, the Agent doesn't support -all the features available to clusters connected through certificates. +The certificate-based integration was used for popular GitLab features like +GitLab Managed Apps, GitLab-managed clusters, and Auto DevOps. +Some features are currently available only when using certificate-based integration. ## Migrate cluster application deployments ### Migrate from GitLab-managed clusters With GitLab-managed clusters, GitLab creates separate service accounts and namespaces -for every branch and deploys using these resources. +for every branch and deploys by using these resources. -To achieve a similar result with the GitLab Agent, you can use [impersonation](../../clusters/agent/ci_cd_tunnel.md#use-impersonation-to-restrict-project-and-group-access) +The GitLab agent uses [impersonation](../../clusters/agent/ci_cd_tunnel.md#use-impersonation-to-restrict-project-and-group-access) strategies to deploy to your cluster with restricted account access. To do so: 1. Choose the impersonation strategy that suits your needs. 1. Use Kubernetes RBAC rules to manage impersonated account permissions in Kubernetes. -1. Use the `access_as` attribute in your Agent’s configuration file to define the impersonation. +1. Use the `access_as` attribute in your agent configuration file to define the impersonation. ### Migrate from Auto DevOps -To configure your Auto DevOps project to use the GitLab Agent: +To configure your Auto DevOps project to use the GitLab agent: -1. Follow the steps to [install an agent](../../clusters/agent/install/index.md) on your cluster. -1. Go to the project in which you use Auto DevOps. -1. From the sidebar, select **Settings > CI/CD** and expand **Variables**. +1. Follow the steps to [install an agent](../../clusters/agent/install/index.md) in your cluster. +1. Go to the project where you use Auto DevOps. +1. On the left sidebar, select **Settings > CI/CD** and expand **Variables**. 1. Select **Add new variable**. 1. Add `KUBE_CONTEXT` as the key, `path/to/agent/project:agent-name` as the value, and select the environment scope of your choice. 1. Select **Add variable**. 1. Repeat the process to add another variable, `KUBE_NAMESPACE`, setting the value for the Kubernetes namespace you want your deployments to target, and set the same environment scope from the previous step. -1. From the sidebar, select **Infrastructure > Kubernetes clusters**. +1. On the left sidebar, select **Infrastructure > Kubernetes clusters**. 1. From the certificate-based clusters section, open the cluster that serves the same environment scope. 1. Select the **Details** tab and disable the cluster. -1. To activate the changes, from the project's sidebar, select **CI/CD > Variables > Run pipeline**. +1. To activate the changes, on the left sidebar, select **CI/CD > Variables > Run pipeline**. -### Migrate generic deployments - -When you use Kubernetes contexts to reach the cluster from GitLab, you can use the [CI/CD Tunnel](../../clusters/agent/ci_cd_tunnel.md) -directly. It injects the available contexts into your CI environment automatically: +For an example, [view this project](https://gitlab.com/gitlab-examples/ops/gitops-demo/hello-world-service). -1. Follow the steps to [install an agent](../../clusters/agent/install/index.md) on your cluster. -1. Go to the project in which you use Auto DevOps. -1. From the sidebar, select **Settings > CI/CD** and expand **Variables**. -1. Select **Add new variable**. -1. Add `KUBE_CONTEXT` as the key, `path/to/agent-configuration-project:your-agent-name` as the value, and select the environment scope of your choice. -1. Edit your `.gitlab-ci.yml` file and set the Kubernetes context to the `KUBE_CONTEXT` you defined in the previous step: +### Migrate generic deployments - ```yaml - <your job name>: - script: - - kubectl config use-context $KUBE_CONTEXT - ``` +Follow the process for the [CI/CD workflow](../../clusters/agent/ci_cd_tunnel.md). -## Migrate from GitLab Managed Applications +## Migrate from GitLab Managed applications -Follow the process to [migrate from GitLab Managed Apps to the Cluster Management Project](../../clusters/migrating_from_gma_to_project_template.md). +Follow the process to [migrate from GitLab Managed Apps to the cluster management project](../../clusters/migrating_from_gma_to_project_template.md). -## Migrating a Cluster Management project +## Migrate a cluster management project -See [how to use a cluster management project with the GitLab Agent](../../clusters/management_project_template.md#use-the-agent-with-the-cluster-management-project-template). +See [how to use a cluster management project with the GitLab agent](../../clusters/management_project_template.md#use-the-agent-with-the-cluster-management-project-template). ## Migrate cluster monitoring features -Cluster monitoring features are not supported by the GitLab Agent for Kubernetes yet. +Cluster monitoring features are not yet supported by the GitLab agent for Kubernetes. diff --git a/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png Binary files differdeleted file mode 100644 index 7e23b7db309..00000000000 --- a/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png +++ /dev/null diff --git a/doc/user/project/repository/img/forking_workflow_choose_namespace_v13_10.png b/doc/user/project/repository/img/forking_workflow_choose_namespace_v13_10.png Binary files differdeleted file mode 100644 index 74f65cb663d..00000000000 --- a/doc/user/project/repository/img/forking_workflow_choose_namespace_v13_10.png +++ /dev/null diff --git a/package.json b/package.json index 6cf702ac7ce..d74b274e6e8 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,9 @@ "@rails/ujs": "6.1.4-6", "@sentry/browser": "5.30.0", "@sourcegraph/code-host-integration": "0.0.60", - "@tiptap/core": "^2.0.0-beta.171", + "@tiptap/core": "^2.0.0-beta.174", "@tiptap/extension-blockquote": "^2.0.0-beta.26", - "@tiptap/extension-bold": "^2.0.0-beta.25", + "@tiptap/extension-bold": "^2.0.0-beta.26", "@tiptap/extension-bullet-list": "^2.0.0-beta.26", "@tiptap/extension-code": "^2.0.0-beta.26", "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68", @@ -76,8 +76,8 @@ "@tiptap/extension-heading": "^2.0.0-beta.26", "@tiptap/extension-history": "^2.0.0-beta.21", "@tiptap/extension-horizontal-rule": "^2.0.0-beta.31", - "@tiptap/extension-image": "^2.0.0-beta.25", - "@tiptap/extension-italic": "^2.0.0-beta.25", + "@tiptap/extension-image": "^2.0.0-beta.27", + "@tiptap/extension-italic": "^2.0.0-beta.26", "@tiptap/extension-link": "^2.0.0-beta.36", "@tiptap/extension-list-item": "^2.0.0-beta.20", "@tiptap/extension-ordered-list": "^2.0.0-beta.27", @@ -156,7 +156,7 @@ "popper.js": "^1.16.1", "portal-vue": "^2.1.7", "prismjs": "^1.21.0", - "prosemirror-markdown": "1.6.0", + "prosemirror-markdown": "1.7.1", "prosemirror-model": "^1.16.1", "prosemirror-state": "^1.3.4", "prosemirror-tables": "^1.1.1", diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index 3d8f832df9e..f466b6f1bc2 100644 --- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -3,11 +3,16 @@ # rubocop:disable Rails/Pluck module QA # Only executes in custom job/pipeline + # https://gitlab.com/gitlab-org/manage/import/import-github-performance + # RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do describe 'Project import' do let(:logger) { Runtime::Logger.logger } let(:differ) { RSpec::Support::Differ.new(color: true) } + let(:created_by_pattern) { /\*Created by: \S+\*\n\n/ } + let(:suggestion_pattern) { /suggestion:-\d+\+\d+/ } + let(:api_client) { Runtime::API::Client.as_admin } let(:user) do @@ -19,46 +24,57 @@ module QA let(:github_repo) { ENV['QA_LARGE_GH_IMPORT_REPO'] || 'rspec/rspec-core' } let(:import_max_duration) { ENV['QA_LARGE_GH_IMPORT_DURATION'] ? ENV['QA_LARGE_GH_IMPORT_DURATION'].to_i : 7200 } let(:github_client) do - Octokit.middleware = Faraday::RackBuilder.new do |builder| - builder.response(:logger, logger, headers: false, bodies: false) - end - Octokit::Client.new( access_token: ENV['QA_LARGE_GH_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token, auto_paginate: true ) end - let(:gh_branches) { github_client.branches(github_repo).map(&:name) } - let(:gh_commits) { github_client.commits(github_repo).map(&:sha) } let(:gh_repo) { github_client.repository(github_repo) } + let(:gh_branches) do + logger.debug("= Fetching branches =") + github_client.branches(github_repo).map(&:name) + end + + let(:gh_commits) do + logger.debug("= Fetching commits =") + github_client.commits(github_repo).map(&:sha) + end + let(:gh_labels) do + logger.debug("= Fetching labels =") github_client.labels(github_repo).map { |label| { name: label.name, color: "##{label.color}" } } end let(:gh_milestones) do + logger.debug("= Fetching milestones =") github_client .list_milestones(github_repo, state: 'all') .map { |ms| { title: ms.title, description: ms.description } } end let(:gh_all_issues) do + logger.debug("= Fetching issues and prs =") github_client.list_issues(github_repo, state: 'all') end let(:gh_prs) do gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash| - hash[pr.title] = { + hash[pr.number] = { + url: pr.html_url, + title: pr.title, body: pr.body || '', - comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact.sort + comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact } end end let(:gh_issues) do gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash| - hash[issue.title] = { + hash[issue.number] = { + url: issue.html_url, + title: issue.title, body: issue.body || '', comments: gh_issue_comments[issue.html_url] } @@ -66,12 +82,14 @@ module QA end let(:gh_issue_comments) do + logger.debug("= Fetching issue comments =") github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key end end let(:gh_pr_comments) do + logger.debug("= Fetching pr comments =") github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key end @@ -97,6 +115,7 @@ module QA "data", { import_time: @import_time, + reported_stats: @stats, github: { project_name: github_repo, branches: gh_branches.length, @@ -104,9 +123,9 @@ module QA labels: gh_labels.length, milestones: gh_milestones.length, prs: gh_prs.length, - pr_comments: gh_prs.sum { |_k, v| v.length }, + pr_comments: gh_prs.sum { |_k, v| v[:comments].length }, issues: gh_issues.length, - issue_comments: gh_issues.sum { |_k, v| v.length } + issue_comments: gh_issues.sum { |_k, v| v[:comments].length } }, gitlab: { project_name: imported_project.path_with_namespace, @@ -115,15 +134,15 @@ module QA labels: gl_labels.length, milestones: gl_milestones.length, mrs: mrs.length, - mr_comments: mrs.sum { |_k, v| v.length }, + mr_comments: mrs.sum { |_k, v| v[:comments].length }, issues: gl_issues.length, - issue_comments: gl_issues.sum { |_k, v| v.length } + issue_comments: gl_issues.sum { |_k, v| v[:comments].length } }, not_imported: { mrs: @mr_diff, issues: @issue_diff } - }.to_json + } ) end @@ -133,19 +152,25 @@ module QA ) do start = Time.now - # import the project and log path - Runtime::Logger.info("Importing project '#{imported_project.reload!.full_path}'") + # import the project and log gitlab path + Runtime::Logger.info("== Importing project '#{github_repo}' in to '#{imported_project.reload!.full_path}' ==") # fetch all objects right after import has started fetch_github_objects import_status = lambda do - imported_project.project_import_status[:import_status].tap do |status| + imported_project.project_import_status.yield_self do |status| + @stats = status.dig(:stats, :imported) + # fail fast if import explicitly failed - raise "Import of '#{imported_project.name}' failed!" if status == 'failed' + raise "Import of '#{imported_project.name}' failed!" if status[:import_status] == 'failed' + + status[:import_status] end end + logger.info("== Waiting for import to be finished ==") expect(import_status).to eventually_eq('finished').within(max_duration: import_max_duration, sleep_interval: 30) + @import_time = Time.now - start aggregate_failures do @@ -161,22 +186,22 @@ module QA # # @return [void] def fetch_github_objects - logger.debug("== Fetching objects for github repo: '#{github_repo}' ==") + logger.info("== Fetching github repo objects ==") gh_repo gh_branches gh_commits - gh_prs - gh_issues gh_labels gh_milestones + gh_prs + gh_issues end # Verify repository imported correctly # # @return [void] def verify_repository_import - logger.debug("== Verifying repository import ==") + logger.info("== Verifying repository import ==") expect(imported_project.description).to eq(gh_repo.description) # check via include, importer creates more branches # https://gitlab.com/gitlab-org/gitlab/-/issues/332711 @@ -184,42 +209,42 @@ module QA expect(gl_commits).to match_array(gh_commits) end - # Verify imported merge requests and mr issues + # Verify imported labels # # @return [void] - def verify_merge_requests_import - logger.debug("== Verifying merge request import ==") - @mr_diff = verify_mrs_or_issues('mr') + def verify_labels_import + logger.info("== Verifying label import ==") + # check via include, additional labels can be inherited from parent group + expect(gl_labels).to include(*gh_labels) end - # Verify imported issues and issue comments + # Verify milestones import # # @return [void] - def verify_issues_import - logger.debug("== Verifying issue import ==") - @issue_diff = verify_mrs_or_issues('issue') + def verify_milestones_import + logger.info("== Verifying milestones import ==") + expect(gl_milestones).to match_array(gh_milestones) end - # Verify imported labels + # Verify imported merge requests and mr issues # # @return [void] - def verify_labels_import - logger.debug("== Verifying label import ==") - # check via include, additional labels can be inherited from parent group - expect(gl_labels).to include(*gh_labels) + def verify_merge_requests_import + logger.info("== Verifying merge request import ==") + @mr_diff = verify_mrs_or_issues('mr') end - # Verify milestones import + # Verify imported issues and issue comments # # @return [void] - def verify_milestones_import - logger.debug("== Verifying milestones import ==") - expect(gl_milestones).to match_array(gh_milestones) + def verify_issues_import + logger.info("== Verifying issue import ==") + @issue_diff = verify_mrs_or_issues('issue') end private - # Verify imported mrs or issues and return diff + # Verify imported mrs or issues and return missing items # # @param [String] type verification object, 'mrs' or 'issues' # @return [Hash] @@ -231,11 +256,10 @@ module QA count_msg = "Expected to contain same amount of #{type}s. Gitlab: #{expected.length}, Github: #{actual.length}" expect(expected.length).to eq(actual.length), count_msg - logger.debug("= Comparing #{type}s =") missing_comments = verify_comments(type, actual, expected) { - "#{type}s": actual.keys - expected.keys, + "#{type}s": (actual.keys - expected.keys).map { |it| actual[it].slice(:title, :url) }, "#{type}_comments": missing_comments } end @@ -247,9 +271,10 @@ module QA # @param [Hash] expected # @return [Hash] def verify_comments(type, actual, expected) - actual.each_with_object({}) do |(title, actual_item), missing_comments| + actual.each_with_object([]) do |(key, actual_item), missing_comments| + expected_item = expected[key] + title = actual_item[:title] msg = "expected #{type} with title '#{title}' to have" - expected_item = expected[title] # Print title in the error message to see which object is missing # @@ -261,9 +286,9 @@ module QA expected_body = expected_item[:body] actual_body = actual_item[:body] body_msg = <<~MSG - #{msg} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])} + #{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)} MSG - expect(expected_body).to include(actual_body), body_msg + expect(expected_body).to eq(actual_body), body_msg # Print amount difference first # @@ -278,7 +303,14 @@ module QA # Save missing comments # comment_diff = actual_comments - expected_comments - missing_comments[title] = comment_diff unless comment_diff.empty? + next if comment_diff.empty? + + missing_comments << { + title: title, + github_url: actual_item[:url], + gitlab_url: expected_item[:url], + missing_comments: comment_diff + } end end @@ -329,20 +361,25 @@ module QA @mrs ||= begin logger.debug("= Fetching merge requests =") imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2) - logger.debug("= Transforming merge request objects for comparison =") - imported_mrs.each_with_object({}) do |mr, hash| + + logger.debug("= Fetching merge request comments =") + imported_mrs.each_with_object({}) do |mr, mrs_with_comments| resource = Resource::MergeRequest.init do |resource| resource.project = imported_project resource.iid = mr[:iid] resource.api_client = api_client end - hash[mr[:title]] = { - body: mr[:description], - comments: resource.comments(auto_paginate: true, attempts: 2) + logger.debug("Fetching comments for mr '#{mr[:title]}'") + mrs_with_comments[mr[:iid]] = { + url: mr[:web_url], + title: mr[:title], + body: sanitize_description(mr[:description]) || '', + comments: resource + .comments(auto_paginate: true, attempts: 2) # remove system notes .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } - .map { |c| sanitize(c[:body]) } + .map { |c| sanitize_comment(c[:body]) } } end end @@ -355,37 +392,51 @@ module QA @gl_issues ||= begin logger.debug("= Fetching issues =") imported_issues = imported_project.issues(auto_paginate: true, attempts: 2) - logger.debug("= Transforming issue objects for comparison =") - imported_issues.each_with_object({}) do |issue, hash| + + logger.debug("= Fetching issue comments =") + imported_issues.each_with_object({}) do |issue, issues_with_comments| resource = Resource::Issue.init do |issue_resource| issue_resource.project = imported_project issue_resource.iid = issue[:iid] issue_resource.api_client = api_client end - hash[issue[:title]] = { - body: issue[:description], - comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) } + logger.debug("Fetching comments for issue '#{issue[:title]}'") + issues_with_comments[issue[:iid]] = { + url: issue[:web_url], + title: issue[:title], + body: sanitize_description(issue[:description]) || '', + comments: resource + .comments(auto_paginate: true, attempts: 2) + .map { |c| sanitize_comment(c[:body]) } } end end end - # Remove added prefixes and legacy diff format + # Remove added prefixes and legacy diff format from comments + # + # @param [String] body + # @return [String] + def sanitize_comment(body) + body.gsub(created_by_pattern, "").gsub(suggestion_pattern, "suggestion\r") + end + + # Remove created by prefix from descripion # # @param [String] body # @return [String] - def sanitize(body) - body.gsub(/\*Created by: \S+\*\n\n/, "").gsub(/suggestion:-\d+\+\d+/, "suggestion\r") + def sanitize_description(body) + body&.gsub(created_by_pattern, "") end # Save json as file # # @param [String] name - # @param [String] json + # @param [Hash] json # @return [void] def save_json(name, json) - File.open("tmp/#{name}.json", "w") { |file| file.write(json) } + File.open("tmp/#{name}.json", "w") { |file| file.write(JSON.pretty_generate(json)) } end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb index 45d466903de..2f142aa7480 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan', :requires_admin, :actioncable, :orchestrated do + RSpec.describe 'Plan', :requires_admin, :actioncable, :orchestrated, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293699', type: :bug } do describe 'Assignees' do let(:user1) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } let(:user2) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) } diff --git a/scripts/gitlab_workhorse_component_helpers.sh b/scripts/gitlab_workhorse_component_helpers.sh new file mode 100644 index 00000000000..06fe7b2ea51 --- /dev/null +++ b/scripts/gitlab_workhorse_component_helpers.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +export CURL_TOKEN_HEADER="${CURL_TOKEN_HEADER:-"JOB-TOKEN"}" +export GITLAB_WORKHORSE_BINARIES_LIST="gitlab-resize-image gitlab-zip-cat gitlab-zip-metadata gitlab-workhorse" +export GITLAB_WORKHORSE_PACKAGE_FILES_LIST="${GITLAB_WORKHORSE_BINARIES_LIST} WORKHORSE_TREE" +export GITLAB_WORKHORSE_TREE=${GITLAB_WORKHORSE_TREE:-$(git rev-parse HEAD:workhorse)} +export GITLAB_WORKHORSE_PACKAGE="workhorse-${GITLAB_WORKHORSE_TREE}.tar.gz" +export GITLAB_WORKHORSE_PACKAGE_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${GITLAB_WORKHORSE_FOLDER}/${GITLAB_WORKHORSE_TREE}/${GITLAB_WORKHORSE_PACKAGE}" + +function gitlab_workhorse_archive_doesnt_exist() { + local package_url="${GITLAB_WORKHORSE_PACKAGE_URL}" + + status=$(curl -I --silent --retry 3 --output /dev/null -w "%{http_code}" "${package_url}") + + [[ "${status}" != "200" ]] +} + +function create_gitlab_workhorse_package() { + local archive_filename="${GITLAB_WORKHORSE_PACKAGE}" + local folder_to_archive="${GITLAB_WORKHORSE_FOLDER}" + local workhorse_folder_path="${TMP_TEST_GITLAB_WORKHORSE_PATH}" + local tar_working_folder="${TMP_TEST_FOLDER}" + + echoinfo "Running 'tar -czvf ${archive_filename} -C ${tar_working_folder} ${folder_to_archive}'" + tar -czvf ${archive_filename} -C ${tar_working_folder} ${folder_to_archive} + du -h ${archive_filename} +} + +function extract_gitlab_workhorse_package() { + local tar_working_folder="${TMP_TEST_FOLDER}" + + echoinfo "Extracting archive to ${tar_working_folder}" + + tar -xzv -C ${tar_working_folder} < /dev/stdin +} + +function upload_gitlab_workhorse_package() { + local archive_filename="${GITLAB_WORKHORSE_PACKAGE}" + local package_url="${GITLAB_WORKHORSE_PACKAGE_URL}" + local token_header="${CURL_TOKEN_HEADER}" + local token="${CI_JOB_TOKEN}" + + echoinfo "Uploading ${archive_filename} to ${package_url} ..." + curl --fail --silent --retry 3 --header "${token_header}: ${token}" --upload-file "${archive_filename}" "${package_url}" +} + +function read_curl_gitlab_workhorse_package() { + local package_url="${GITLAB_WORKHORSE_PACKAGE_URL}" + local token_header="${CURL_TOKEN_HEADER}" + local token="${CI_JOB_TOKEN}" + + echoinfo "Downloading from ${package_url} ..." + + curl --fail --silent --retry 3 --header "${token_header}: ${token}" "${package_url}" +} + +function download_and_extract_gitlab_workhorse_package() { + read_curl_gitlab_workhorse_package | extract_gitlab_workhorse_package +} + +function select_gitlab_workhorse_essentials() { + local tmp_path="${CI_PROJECT_DIR}/tmp/${GITLAB_WORKHORSE_FOLDER}" + local original_gitlab_workhorse_path="${TMP_TEST_GITLAB_WORKHORSE_PATH}" + + mkdir -p ${tmp_path} + cd ${original_gitlab_workhorse_path} && mv ${GITLAB_WORKHORSE_PACKAGE_FILES_LIST} ${tmp_path} && cd - + rm -rf ${original_gitlab_workhorse_path} + + # Move the temp folder to its final destination + mv ${tmp_path} ${TMP_TEST_FOLDER} +} diff --git a/scripts/utils.sh b/scripts/utils.sh index c20508617b8..e896fe40e06 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -83,7 +83,7 @@ function install_junit_merge_gem() { function run_timed_command() { local cmd="${1}" - local metric_name="${2}" + local metric_name="${2:-no}" local timed_metric_file local start=$(date +%s) @@ -97,7 +97,7 @@ function run_timed_command() { if [[ $ret -eq 0 ]]; then echosuccess "==> '${cmd}' succeeded in ${runtime} seconds." - if [[ -n "${metric_name}" ]]; then + if [[ "${metric_name}" != "no" ]]; then timed_metric_file=$(timed_metric_file $metric_name) echo "# TYPE ${metric_name} gauge" > "${timed_metric_file}" echo "# UNIT ${metric_name} seconds" >> "${timed_metric_file}" @@ -132,9 +132,9 @@ function timed_metric_file() { } function echoerr() { - local header="${2}" + local header="${2:-no}" - if [ -n "${header}" ]; then + if [ "${header}" != "no" ]; then printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2; else printf "\033[0;31m%s\n\033[0m" "${1}" >&2; @@ -142,9 +142,9 @@ function echoerr() { } function echoinfo() { - local header="${2}" + local header="${2:-no}" - if [ -n "${header}" ]; then + if [ "${header}" != "no" ]; then printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2; else printf "\033[0;33m%s\n\033[0m" "${1}" >&2; @@ -152,9 +152,9 @@ function echoinfo() { } function echosuccess() { - local header="${2}" + local header="${2:-no}" - if [ -n "${header}" ]; then + if [ "${header}" != "no" ]; then printf "\n\033[0;32m** %s **\n\033[0m" "${1}" >&2; else printf "\033[0;32m%s\n\033[0m" "${1}" >&2; diff --git a/spec/frontend/lib/utils/ignore_while_pending_spec.js b/spec/frontend/lib/utils/ignore_while_pending_spec.js new file mode 100644 index 00000000000..b68ba936dde --- /dev/null +++ b/spec/frontend/lib/utils/ignore_while_pending_spec.js @@ -0,0 +1,136 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending'; + +const TEST_ARGS = [123, { foo: 'bar' }]; + +describe('~/lib/utils/ignore_while_pending', () => { + let spyResolve; + let spyReject; + let spy; + let subject; + + beforeEach(() => { + spy = jest.fn().mockImplementation( + // NOTE: We can't pass an arrow function here... + function foo() { + return new Promise((resolve, reject) => { + spyResolve = resolve; + spyReject = reject; + }); + }, + ); + }); + + describe('with non-instance method', () => { + beforeEach(() => { + subject = ignoreWhilePending(spy); + }); + + it('while pending, will ignore subsequent calls', () => { + subject(...TEST_ARGS); + subject(); + subject(); + subject(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(...TEST_ARGS); + }); + + it.each` + desc | act + ${'when resolved'} | ${() => spyResolve()} + ${'when rejected'} | ${() => spyReject(new Error('foo'))} + `('$desc, can be triggered again', async ({ act }) => { + // We need the empty catch(), since we are testing rejecting the promise, + // which would otherwise cause the test to fail. + subject(...TEST_ARGS).catch(() => {}); + subject(); + subject(); + subject(); + + act(); + // We need waitForPromises, so that the underlying finally() runs. + await waitForPromises(); + + subject({ again: 'foo' }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(...TEST_ARGS); + expect(spy).toHaveBeenCalledWith({ again: 'foo' }); + }); + + it('while pending, returns empty resolutions for ignored calls', async () => { + subject(...TEST_ARGS); + + await expect(subject(...TEST_ARGS)).resolves.toBeUndefined(); + await expect(subject(...TEST_ARGS)).resolves.toBeUndefined(); + }); + + it('when resolved, returns resolution for origin call', async () => { + const resolveValue = { original: 1 }; + const result = subject(...TEST_ARGS); + + spyResolve(resolveValue); + + await expect(result).resolves.toEqual(resolveValue); + }); + + it('when rejected, returns rejection for original call', async () => { + const rejectedErr = new Error('original'); + const result = subject(...TEST_ARGS); + + spyReject(rejectedErr); + + await expect(result).rejects.toEqual(rejectedErr); + }); + }); + + describe('with instance method', () => { + let instance1; + let instance2; + + beforeEach(() => { + // Let's capture the "this" for tests + subject = ignoreWhilePending(function instanceMethod(...args) { + return spy(this, ...args); + }); + + instance1 = {}; + instance2 = {}; + }); + + it('will not ignore calls across instances', () => { + subject.call(instance1, { context: 1 }); + subject.call(instance1, {}); + subject.call(instance1, {}); + subject.call(instance2, { context: 2 }); + subject.call(instance2, {}); + + expect(spy.mock.calls).toEqual([ + [instance1, { context: 1 }], + [instance2, { context: 2 }], + ]); + }); + + it('resolving one instance does not resolve other instances', async () => { + subject.call(instance1, { context: 1 }); + + // We need to save off spyResolve so it's not overwritten by next call + const instance1Resolve = spyResolve; + + subject.call(instance2, { context: 2 }); + + instance1Resolve(); + await waitForPromises(); + + subject.call(instance1, { context: 1 }); + subject.call(instance2, { context: 2 }); + + expect(spy.mock.calls).toEqual([ + [instance1, { context: 1 }], + [instance2, { context: 2 }], + [instance1, { context: 1 }], + ]); + }); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/input_spec.js b/spec/frontend/pipeline_wizard/components/input_spec.js new file mode 100644 index 00000000000..ee1f3fe70ff --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/input_spec.js @@ -0,0 +1,79 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import { Document } from 'yaml'; +import InputWrapper from '~/pipeline_wizard/components/input.vue'; +import TextWidget from '~/pipeline_wizard/components/widgets/text.vue'; + +describe('Pipeline Wizard -- Input Wrapper', () => { + let wrapper; + + const createComponent = (props = {}, mountFunc = mount) => { + wrapper = mountFunc(InputWrapper, { + propsData: { + template: new Document({ + template: { + bar: 'baz', + foo: { some: '$TARGET' }, + }, + }).get('template'), + compiled: new Document({ bar: 'baz', foo: { some: '$TARGET' } }), + target: '$TARGET', + widget: 'text', + label: 'some label (required by the text widget)', + ...props, + }, + }); + }; + + describe('API', () => { + const inputValue = 'dslkfjsdlkfjlskdjfn'; + let inputChild; + + beforeEach(() => { + createComponent({}); + inputChild = wrapper.find(TextWidget); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('will replace its value in compiled', async () => { + await inputChild.vm.$emit('input', inputValue); + const expected = new Document({ + bar: 'baz', + foo: { some: inputValue }, + }); + expect(wrapper.emitted()['update:compiled']).toEqual([[expected]]); + }); + + it('will emit a highlight event with the correct path if child emits an input event', async () => { + await inputChild.vm.$emit('input', inputValue); + const expected = ['foo', 'some']; + expect(wrapper.emitted().highlight).toEqual([[expected]]); + }); + }); + + describe('Target Path Discovery', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + scenario | template | target | expected + ${'simple nested object'} | ${{ foo: { bar: { baz: '$BOO' } } }} | ${'$BOO'} | ${['foo', 'bar', 'baz']} + ${'list, first pos.'} | ${{ foo: ['$BOO'] }} | ${'$BOO'} | ${['foo', 0]} + ${'list, second pos.'} | ${{ foo: ['bar', '$BOO'] }} | ${'$BOO'} | ${['foo', 1]} + ${'lowercase target'} | ${{ foo: { bar: '$jupp' } }} | ${'$jupp'} | ${['foo', 'bar']} + ${'root list'} | ${['$BOO']} | ${'$BOO'} | ${[0]} + `('$scenario', ({ template, target, expected }) => { + createComponent( + { + template: new Document({ template }).get('template'), + target, + }, + shallowMount, + ); + expect(wrapper.vm.path).toEqual(expected); + }); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/widgets_spec.js b/spec/frontend/pipeline_wizard/components/widgets_spec.js new file mode 100644 index 00000000000..5944c76c5d0 --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/widgets_spec.js @@ -0,0 +1,49 @@ +import fs from 'fs'; +import { mount } from '@vue/test-utils'; +import { Document } from 'yaml'; +import InputWrapper from '~/pipeline_wizard/components/input.vue'; + +describe('Test all widgets in ./widgets/* whether they provide a minimal api', () => { + const createComponent = (props = {}, mountFunc = mount) => { + mountFunc(InputWrapper, { + propsData: { + template: new Document({ + template: { + bar: 'baz', + foo: { some: '$TARGET' }, + }, + }).get('template'), + compiled: new Document({ bar: 'baz', foo: { some: '$TARGET' } }), + target: '$TARGET', + widget: 'text', + label: 'some label (required by the text widget)', + ...props, + }, + }); + }; + + const widgets = fs + .readdirSync('./app/assets/javascripts/pipeline_wizard/components/widgets') + .map((filename) => [filename.match(/^(.*).vue$/)[1]]); + let consoleErrorSpy; + + beforeAll(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterAll(() => { + consoleErrorSpy.mockRestore(); + }); + + describe.each(widgets)('`%s` Widget', (name) => { + it('passes the input validator', () => { + const validatorFunc = InputWrapper.props.widget.validator; + expect(validatorFunc(name)).toBe(true); + }); + + it('mounts without error', () => { + createComponent({ widget: name }); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ae3aafffe56..e3d468c1b5d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -55,6 +55,7 @@ issues: - status_page_published_incident - namespace - note_authors +- user_note_authors - issue_email_participants - test_reports - requirement @@ -200,6 +201,7 @@ merge_requests: - user_mentions - system_note_metadata - note_authors +- user_note_authors - cleanup_schedule - compliance_violations external_pull_requests: @@ -774,6 +776,7 @@ epic: - resource_state_events - user_mentions - note_authors +- user_note_authors - boards_epic_user_preferences - epic_board_positions epic_issue: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f019883a91e..e06fcb0cd3f 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -857,6 +857,7 @@ Epic: - health_status - external_key - confidential + - color EpicIssue: - id - relative_position diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 39199dac980..cf405c08ec8 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -18,7 +18,6 @@ RSpec.describe Issuable do it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:todos) } it { is_expected.to have_many(:labels) } - it { is_expected.to have_many(:note_authors).through(:notes) } context 'Notes' do let!(:note) { create(:note, noteable: issue, project: issue.project) } @@ -28,6 +27,23 @@ RSpec.describe Issuable do expect(issue.notes).not_to be_authors_loaded expect(scoped_issue.notes).to be_authors_loaded end + + describe 'note_authors' do + it { is_expected.to have_many(:note_authors).through(:notes) } + end + + describe 'user_note_authors' do + let_it_be(:system_user) { create(:user) } + + let!(:system_note) { create(:system_note, author: system_user, noteable: issue, project: issue.project) } + + it 'filters the authors to those of user notes' do + authors = issue.user_note_authors + + expect(authors).to include(note.author) + expect(authors).not_to include(system_user) + end + end end end diff --git a/yarn.lock b/yarn.lock index 7a27ed99aba..1e1880e702e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1542,10 +1542,10 @@ dom-accessibility-api "^0.5.1" pretty-format "^26.4.2" -"@tiptap/core@^2.0.0-beta.171": - version "2.0.0-beta.171" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.171.tgz#e681964c443383b81d2638c51fc3bbfda034a4fb" - integrity sha512-4CdJfcchmBOFooWPBMJ7AxJISeTstMFriQv0RyReMt0Dpef/c9UoU+NkKLwwv5VRUX0M8dL5SzEhkB8wIODqlA== +"@tiptap/core@^2.0.0-beta.174": + version "2.0.0-beta.174" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.174.tgz#cfdf16b7d7401e4b255dc69147d784f5f537b942" + integrity sha512-APQDto40PdvagG1HTwkKlieQS4Vp6GXNe7qgV1Qo2QCgJCLyxc/fXCTghtrOx0CQb+9JT7fjSLZxbSyUFXjx7Q== dependencies: "@types/prosemirror-commands" "^1.0.4" "@types/prosemirror-keymap" "^1.0.4" @@ -1567,10 +1567,10 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.26.tgz#e5ae4b7bd9376db37407a23e22080c7b11287f3b" integrity sha512-A6yjcYovONJfOjQFk6vDYXswaCdCtCwjL7w9VTB0R2DLTuJvvRt9DWN0IDcMrj5G+aMgDq4GUUTitv+2Y8krDg== -"@tiptap/extension-bold@^2.0.0-beta.25": - version "2.0.0-beta.25" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.25.tgz#ec19e7c862d25bae49609c5d6a873f372c506dee" - integrity sha512-ZNdgFYDxKo8lAp0Pqzu45I0JH3ah8/X5TCYg9zNg3QwLUFT16g2LlWDMUDGT5pH9aXxgtFaEdoVacu0EyhlPnQ== +"@tiptap/extension-bold@^2.0.0-beta.26": + version "2.0.0-beta.26" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.26.tgz#aa1c7850df28cec8e0614fde437183bd4ae3e66b" + integrity sha512-pnO0I5sEQM3pmowjMGQ74adLzvc6HqGyLyqMizaGMicPu9uTYlSdId+qckYEEgPwPMaEShtv2Vg+ZHs7KVqfcg== "@tiptap/extension-bubble-menu@^2.0.0-beta.55": version "2.0.0-beta.55" @@ -1665,15 +1665,15 @@ dependencies: prosemirror-state "^1.3.4" -"@tiptap/extension-image@^2.0.0-beta.25": - version "2.0.0-beta.25" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.25.tgz#7fb001a6449a9a841ae4f42c258ad6a06022b523" - integrity sha512-RgW5jFVS2QNDvFhBOz7H1hY6LjYcbVAa/mE4F4c3RPg3o7GJZXNoL9s+k0QkEM2GXAvY6fX+OICMBn8TSENXKA== +"@tiptap/extension-image@^2.0.0-beta.27": + version "2.0.0-beta.27" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.27.tgz#62152240cfa7ead03080c38485c1ebda4a603d18" + integrity sha512-kdJ7V39yNdVWUco/RBe7WgvFevd81l+pU6+Je9HpelqBBP953wDttzLMuAWQB4AeLv9WhKSlORHiFv2SKsV5NA== -"@tiptap/extension-italic@^2.0.0-beta.25": - version "2.0.0-beta.25" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.25.tgz#c2ec95cc5baf855134883c5e261da4ab0d3b9479" - integrity sha512-7PvhioTX9baVp5+AmmZU0qna+dFPZCRlSEN/GciH57N77d2uhJ/ZW5iQWTbvy5HBNddQB4Jts1UDIaC7WASrGA== +"@tiptap/extension-italic@^2.0.0-beta.26": + version "2.0.0-beta.26" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.26.tgz#b00c9e32b81b1bd94eaed24bb2a22e44d5dc54a3" + integrity sha512-vejGe2ra4K5ipFOn1U9viqF9X9nPTX8WSJpSOux+9UbKjHpANy7bz69tp66OIi/Wh5L/MMDc+luH/04qfVnpZw== "@tiptap/extension-link@^2.0.0-beta.36": version "2.0.0-beta.36" @@ -5130,11 +5130,6 @@ entities@^2.0.0, entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -entities@~2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== - envinfo@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -7866,13 +7861,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -linkify-it@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" - integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== - dependencies: - uc.micro "^1.0.1" - linkify-it@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" @@ -8192,7 +8180,7 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it@12.3.2: +markdown-it@12.3.2, markdown-it@^12.0.0: version "12.3.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== @@ -8203,17 +8191,6 @@ markdown-it@12.3.2: mdurl "^1.0.1" uc.micro "^1.0.5" -markdown-it@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" - integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== - dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^2.0.0" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdownlint-cli@0.31.0: version "0.31.0" resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.31.0.tgz#a44264a71066475228292b7af19d3d18b827676d" @@ -9667,12 +9644,12 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.1.4, prosemirror-state "^1.0.0" w3c-keyname "^2.2.0" -prosemirror-markdown@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.6.0.tgz#141c88e03c8892f2e93cf58b1382ab0b6088d012" - integrity sha512-y/gRpJIIrNArtkyMax7ypYafb+ZMjddbVHI+AwlcUfCLCCXK57cOmfBMKYVq9kdEKJYVdYHdoyWsVNn1nWLHUg== +prosemirror-markdown@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.7.1.tgz#811a4846da1b3bb661ceb4b37efb89cc4f0cd4b8" + integrity sha512-d1lNRPlbwuncErkR/fv2n3ZEhAoS+0udByM2mZkyDHeY3ux3bUnj7J/ep1XS0FiXUjffixTKI0IAax3H82JbEg== dependencies: - markdown-it "^10.0.0" + markdown-it "^12.0.0" prosemirror-model "^1.0.0" prosemirror-model@^1.0.0, prosemirror-model@^1.13.1, prosemirror-model@^1.16.0, prosemirror-model@^1.16.1, prosemirror-model@^1.2.0, prosemirror-model@^1.8.1: |