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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml63
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml10
-rw-r--r--app/assets/javascripts/issues/show/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/ignore_while_pending.js26
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue5
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue8
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/input.vue99
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/feature_flags/development/fix_comment_scroll.yml8
-rw-r--r--db/migrate/20211021115409_add_color_to_epics.rb10
-rw-r--r--db/migrate/20211021124715_add_text_limit_to_epics_color.rb13
-rw-r--r--db/schema_migrations/202110211154091
-rw-r--r--db/schema_migrations/202110211247151
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/epics.md9
-rw-r--r--doc/api/graphql/reference/index.md6
-rw-r--r--doc/development/documentation/styleguide/index.md11
-rw-r--r--doc/user/analytics/img/mr_throughput_chart_v13_3.pngbin17870 -> 0 bytes
-rw-r--r--doc/user/analytics/img/project_vsa_filter_v14_3.pngbin21120 -> 0 bytes
-rw-r--r--doc/user/analytics/img/project_vsa_stage_table_v14_4.pngbin38783 -> 0 bytes
-rw-r--r--doc/user/clusters/img/kubernetes-agent-ui-list_v14_8.pngbin22175 -> 0 bytes
-rw-r--r--doc/user/group/import/img/import_panel_v14_1.pngbin17447 -> 0 bytes
-rw-r--r--doc/user/group/import/img/new_group_navigation_v13_8.pngbin8644 -> 0 bytes
-rw-r--r--doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md83
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_v12_2.pngbin60405 -> 0 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_choose_namespace_v13_10.pngbin18552 -> 0 bytes
-rw-r--r--package.json10
-rw-r--r--qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb177
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb2
-rw-r--r--scripts/gitlab_workhorse_component_helpers.sh73
-rw-r--r--scripts/utils.sh16
-rw-r--r--spec/frontend/lib/utils/ignore_while_pending_spec.js136
-rw-r--r--spec/frontend/pipeline_wizard/components/input_spec.js79
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets_spec.js49
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/concerns/issuable_spec.rb18
-rw-r--r--yarn.lock67
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
deleted file mode 100644
index 100c9a8557c..00000000000
--- a/doc/user/analytics/img/mr_throughput_chart_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/analytics/img/project_vsa_filter_v14_3.png b/doc/user/analytics/img/project_vsa_filter_v14_3.png
deleted file mode 100644
index d3fcad31909..00000000000
--- a/doc/user/analytics/img/project_vsa_filter_v14_3.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index e65296062f6..00000000000
--- a/doc/user/analytics/img/project_vsa_stage_table_v14_4.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 5b5ba3a9804..00000000000
--- a/doc/user/clusters/img/kubernetes-agent-ui-list_v14_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/import/img/import_panel_v14_1.png b/doc/user/group/import/img/import_panel_v14_1.png
deleted file mode 100644
index 52791a82c3c..00000000000
--- a/doc/user/group/import/img/import_panel_v14_1.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 40be3dd41d2..00000000000
--- a/doc/user/group/import/img/new_group_navigation_v13_8.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 7e23b7db309..00000000000
--- a/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 74f65cb663d..00000000000
--- a/doc/user/project/repository/img/forking_workflow_choose_namespace_v13_10.png
+++ /dev/null
Binary files differ
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: