summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-10 12:09:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-10 12:09:12 +0000
commit0e0df204c1a0d859ccbbe1be83a5e09a53381f17 (patch)
treee7bf6fed5fa2b74caf31957c468b0cbc303f4c45
parenta2344dbf1942dc3919c55b0684d2566368e03852 (diff)
downloadgitlab-ce-0e0df204c1a0d859ccbbe1be83a5e09a53381f17.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml89
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml253
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml282
-rw-r--r--.gitlab/ci/qa-common/rules.gitlab-ci.yml (renamed from .gitlab/ci/package-and-test/rules.gitlab-ci.yml)8
-rw-r--r--.gitlab/ci/qa-common/variables.gitlab-ci.yml (renamed from .gitlab/ci/package-and-test/variables.gitlab-ci.yml)0
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml93
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml15
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/test-on-gdk/main.gitlab-ci.yml137
-rw-r--r--.rubocop_todo/rspec/shared_groups_metadata.yml1
-rw-r--r--.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml2
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue23
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue54
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_alert.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue11
-rw-r--r--app/assets/javascripts/content_editor/extensions/attachment.js34
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/loading.js24
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js6
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js2
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js8
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js214
-rw-r--r--app/assets/javascripts/content_editor/services/utils.js22
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js4
-rw-r--r--app/assets/javascripts/diffs/utils/merge_request.js10
-rw-r--r--app/assets/javascripts/lib/utils/tappable_promise.js49
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue11
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue3
-rw-r--r--app/assets/stylesheets/components/content_editor.scss14
-rw-r--r--app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb4
-rw-r--r--config/metrics/license/20230505112412_installation_creation_date_approximation.yml22
-rw-r--r--data/removals/16_0/16-0-graphql-dora-environment-tier-param.yml14
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt3
-rw-r--r--doc/api/dora/metrics.md2
-rw-r--r--doc/api/graphql/reference/index.md85
-rw-r--r--doc/ci/pipelines/index.md1
-rw-r--r--doc/development/api_graphql_styleguide.md4
-rw-r--r--doc/development/i18n/externalization.md4
-rw-r--r--doc/integration/jira/issues.md28
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/update/removals.md10
-rw-r--r--doc/user/profile/index.md15
-rw-r--r--doc/user/project/remote_development/index.md17
-rw-r--r--doc/user/workspace/index.md198
-rw-r--r--glfm_specification/output_example_snapshots/html.yml18
-rw-r--r--glfm_specification/output_example_snapshots/prosemirror_json.yml156
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb2
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb4
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric.rb15
-rw-r--r--lib/tasks/gitlab/assets.rake4
-rw-r--r--locale/gitlab.pot6
-rwxr-xr-xqa/gdk/launch1
-rw-r--r--rubocop/cop/rspec/avoid_conditional_statements.rb4
-rwxr-xr-xscripts/generate-e2e-pipeline37
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js34
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js99
-rw-r--r--spec/frontend/content_editor/components/toolbar_attachment_button_spec.js13
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js546
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js5
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js21
-rw-r--r--spec/frontend/content_editor/test_utils.js25
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js38
-rw-r--r--spec/frontend/lib/utils/tappable_promise_spec.js63
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js17
-rw-r--r--spec/frontend/monitoring/mock_data.js26
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js6
-rw-r--r--spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb6
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb9
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric_spec.rb22
-rw-r--r--spec/lib/gitlab_spec.rb22
-rw-r--r--spec/models/container_repository_spec.rb24
-rw-r--r--spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb6
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb3
82 files changed, 2211 insertions, 836 deletions
diff --git a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml
new file mode 100644
index 00000000000..841818d9e0f
--- /dev/null
+++ b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml
@@ -0,0 +1,89 @@
+include:
+ - local: .gitlab/ci/qa-common/main.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml
+
+workflow:
+ rules:
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "nightly"'
+
+.ce:
+ variables:
+ RELEASE: ${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ce:${CI_COMMIT_SHA}
+
+.ee:
+ variables:
+ RELEASE: ${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}
+
+# ==========================================
+# Prepare stage
+# ==========================================
+# TODO: enable once ee jobs are added
+# trigger-omnibus-env:
+# extends:
+# - .trigger-omnibus-env
+
+trigger-omnibus-env-ce:
+ extends:
+ - .trigger-omnibus-env-ce
+ variables:
+ FOSS_ONLY: "1" # set FOSS_ONLY because we don't pass it via trigger job
+
+# TODO: enable once ee jobs are added
+# trigger-omnibus:
+# extends:
+# - .trigger-omnibus
+# needs:
+# - trigger-omnibus-env
+
+trigger-omnibus-ce:
+ extends:
+ - .trigger-omnibus-ce
+ needs:
+ - trigger-omnibus-env-ce
+
+# TODO: enable when first parallel job is added
+# download-knapsack-report:
+# extends:
+# - .download-knapsack-report
+# - .rules:download-knapsack
+
+# ==========================================
+# Test stage
+# ==========================================
+update-ee-to-ce:
+ extends:
+ - .qa
+ - .update-script
+ - .ce
+ variables:
+ UPDATE_TYPE: minor
+ UPDATE_FROM_EDITION: ee
+ QA_RSPEC_TAGS: --tag smoke
+
+# ==========================================
+# Post test stage
+# ==========================================
+e2e-test-report:
+ extends:
+ - .e2e-test-report
+
+# TODO: enable when first parallel job is added
+# upload-knapsack-report:
+# extends:
+# - .upload-knapsack-report
+# - .rules:report:process-results
+
+export-test-metrics:
+ extends:
+ - .export-test-metrics
+
+relate-test-failures:
+ extends:
+ - .relate-test-failures
+
+notify-slack:
+ extends:
+ - .notify-slack
+ variables:
+ TYPE: "(nightly) "
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index a53c195e025..774939c7e44 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -1,76 +1,9 @@
# E2E tests pipeline loaded dynamically by script: scripts/generate-e2e-pipeline
# For adding new tests, refer to: doc/development/testing_guide/end_to_end/package_and_test_pipeline.md
-
-default:
- interruptible: true
-
-workflow:
- name: $PIPELINE_NAME
-
include:
- - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
- - project: gitlab-org/quality/pipeline-common
- ref: 3.1.5
- file:
- - /ci/base.gitlab-ci.yml
- - /ci/allure-report.yml
- - /ci/knapsack-report.yml
-
-stages:
- - test
- - report
- - notify
-
-# ==========================================
-# Templates
-# ==========================================
-.parallel:
- parallel: 5
- variables:
- QA_KNAPSACK_REPORT_PATH: $CI_PROJECT_DIR/qa/knapsack
-
-.ruby-image:
- # Because this pipeline template can be included directly in other projects,
- # image path and registry needs to be defined explicitly
- image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3
-
-.qa-install:
- variables:
- BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true"
- BUNDLE_SILENCE_ROOT_WARNING: "true"
- extends:
- - .gitlab-qa-install
-
-.update-script:
- script:
- - !reference [.bundle-prefix]
- - export QA_COMMAND="$BUNDLE_PREFIX gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $GITLAB_SEMVER_VERSION $UPDATE_TYPE -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS"
- - echo "Running - '$QA_COMMAND'"
- - eval "$QA_COMMAND"
-
-.qa:
- extends:
- - .qa-base
- - .qa-install
- - .gitlab-qa-report
- stage: test
- tags:
- - e2e
- variables:
- QA_GENERATE_ALLURE_REPORT: "true"
- QA_CAN_TEST_PRAEFECT: "false"
- QA_INTERCEPT_REQUESTS: "true"
- GITLAB_LICENSE_MODE: test
- GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
- GITLAB_QA_OPTS: $EXTRA_GITLAB_QA_OPTS
- # todo: remove in 16.1 milestone when not needed for backwards compatibility anymore
- EE_LICENSE: $QA_EE_LICENSE
- GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN
- # Allow QA jobs to fail as they are flaky. The top level `package-and-e2e:ee`
- # pipeline is not allowed to fail, so without allowing QA to fail, we will be
- # blocking merges due to flaky tests.
- allow_failure: true
+ - local: .gitlab/ci/qa-common/main.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml
# ==========================================
# Prepare stage
@@ -87,128 +20,34 @@ check-release-set:
echo "Omnibus installation image is set to '$RELEASE'"
fi
-dont-interrupt-me:
- extends: .rules:dont-interrupt
- stage: .pre
- interruptible: false
- script:
- - echo "This jobs makes sure this pipeline won't be interrupted! See https://docs.gitlab.com/ee/ci/yaml/#interruptible."
-
trigger-omnibus-env:
extends:
+ - .trigger-omnibus-env
- .rules:omnibus-build
- stage: .pre
- needs:
- # We need this job because we need its `cached-assets-hash.txt` artifact, so that we can pass the assets image tag to the downstream omnibus-gitlab pipeline.
- - pipeline: $PARENT_PIPELINE_ID
- job: build-assets-image
- variables:
- BUILD_ENV: build.env
- before_script:
- - |
- # This is duplicating the function from `scripts/utils.sh` since `.gitlab/ci/package-and-test/main.gitlab-ci.yml` can be included in other projects.
- function assets_image_tag() {
- local cache_assets_hash_file="cached-assets-hash.txt"
-
- if [[ -n "${CI_COMMIT_TAG}" ]]; then
- echo -n "${CI_COMMIT_REF_NAME}"
- elif [[ -f "${cache_assets_hash_file}" ]]; then
- echo -n "assets-hash-$(cat ${cache_assets_hash_file} | cut -c1-10)"
- else
- echo -n "${CI_COMMIT_SHA}"
- fi
- }
- script:
- - |
- SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true")
- echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV
- echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV
- for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done
- echo "OMNIBUS_GITLAB_RUBY3_BUILD=${OMNIBUS_GITLAB_RUBY3_BUILD:-false}" >> $BUILD_ENV
- echo "OMNIBUS_GITLAB_RUBY2_BUILD=${OMNIBUS_GITLAB_RUBY2_BUILD:-false}" >> $BUILD_ENV
- echo "OMNIBUS_GITLAB_CACHE_EDITION=${OMNIBUS_GITLAB_CACHE_EDITION:-GITLAB}" >> $BUILD_ENV
- echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV
- echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV
- echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
- target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}"
- echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV
- - |
- echo "Built environment file for omnibus build:"
- cat $BUILD_ENV
- artifacts:
- expire_in: 3 days
- reports:
- dotenv: $BUILD_ENV
- paths:
- - $BUILD_ENV
trigger-omnibus-env-ce:
extends:
- - trigger-omnibus-env
+ - .trigger-omnibus-env-ce
- .rules:omnibus-build-ce
- needs:
- - pipeline: $PARENT_PIPELINE_ID
- job: build-assets-image as-if-foss
trigger-omnibus:
- extends: .rules:omnibus-build
- stage: .pre
+ extends:
+ - .trigger-omnibus
+ - .rules:omnibus-build
needs:
- trigger-omnibus-env
- inherit:
- variables: false
- variables:
- GITALY_SERVER_VERSION: $GITALY_SERVER_VERSION
- GITLAB_ELASTICSEARCH_INDEXER_VERSION: $GITLAB_ELASTICSEARCH_INDEXER_VERSION
- GITLAB_KAS_VERSION: $GITLAB_KAS_VERSION
- GITLAB_METRICS_EXPORTER_VERSION: $GITLAB_METRICS_EXPORTER_VERSION
- GITLAB_PAGES_VERSION: $GITLAB_PAGES_VERSION
- GITLAB_SHELL_VERSION: $GITLAB_SHELL_VERSION
- GITLAB_WORKHORSE_VERSION: $GITLAB_WORKHORSE_VERSION
- GITLAB_VERSION: $CI_COMMIT_SHA
- GITLAB_ASSETS_TAG: $GITLAB_ASSETS_TAG
- IMAGE_TAG: $CI_COMMIT_SHA
- TOP_UPSTREAM_SOURCE_PROJECT: $CI_PROJECT_PATH
- SECURITY_SOURCES: $SECURITY_SOURCES
- CACHE_UPDATE: $OMNIBUS_GITLAB_CACHE_UPDATE
- RUBY3_BUILD: $OMNIBUS_GITLAB_RUBY3_BUILD
- RUBY2_BUILD: $OMNIBUS_GITLAB_RUBY2_BUILD
- CACHE_EDITION: $OMNIBUS_GITLAB_CACHE_EDITION
- BUILD_ON_ALL_OS: $OMNIBUS_GITLAB_BUILD_ON_ALL_OS
- SKIP_QA_TEST: "true"
- ee: $EE
- trigger:
- project: gitlab-org/build/omnibus-gitlab-mirror
- branch: $TRIGGER_BRANCH
- strategy: depend
trigger-omnibus-ce:
extends:
- - trigger-omnibus
+ - .trigger-omnibus-ce
- .rules:omnibus-build-ce
- variables:
- # Override gitlab repository so that omnibus doesn't use foss repository for CE build
- GITLAB_ALTERNATIVE_REPO: $CI_PROJECT_URL
needs:
- trigger-omnibus-env-ce
download-knapsack-report:
extends:
- - .gitlab-qa-image
+ - .download-knapsack-report
- .rules:download-knapsack
- stage: .pre
- variables:
- KNAPSACK_DIR: ${CI_PROJECT_DIR}/qa/knapsack
- GIT_STRATEGY: none
- script:
- # when using qa-image, code runs in /home/gitlab/qa folder
- - bundle exec rake "knapsack:download[test]"
- - mkdir -p "$KNAPSACK_DIR" && cp knapsack/*.json "${KNAPSACK_DIR}/"
- allow_failure: true
- artifacts:
- paths:
- - qa/knapsack/*.json
- expire_in: 1 day
cache-gems:
extends:
@@ -657,92 +496,32 @@ importers:
# ==========================================
e2e-test-report:
extends:
- - .generate-allure-report-base
+ - .e2e-test-report
- .rules:report:allure-report
- stage: report
- variables:
- GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
- ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
- ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
upload-knapsack-report:
extends:
- - .generate-knapsack-report-base
- - .qa-install
- - .ruby-image
+ - .upload-knapsack-report
- .rules:report:process-results
- stage: report
- when: always
export-test-metrics:
extends:
- - .qa-install
- - .ruby-image
+ - .export-test-metrics
- .rules:report:process-results
- stage: report
- when: always
- script:
- - bundle exec rake "ci:export_test_metrics[$CI_PROJECT_DIR/gitlab-qa-run-*/**/test-metrics-*.json]"
relate-test-failures:
extends:
- - .qa-install
- - .ruby-image
+ - .relate-test-failures
- .rules:report:process-results
- stage: report
- variables:
- QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
- QA_FAILURES_MAX_DIFF_RATIO: "0.15"
- GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN
- when: on_failure
- script:
- - |
- bundle exec gitlab-qa-report \
- --relate-failure-issue "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json" \
- --project "$QA_FAILURES_REPORTING_PROJECT" \
- --max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO"
generate-test-session:
extends:
- - .qa-install
- - .ruby-image
+ - .generate-test-session
- .rules:report:process-results
- stage: report
- variables:
- QA_TESTCASE_SESSIONS_PROJECT: gitlab-org/quality/testcase-sessions
- GITLAB_QA_ACCESS_TOKEN: $QA_TEST_SESSION_TOKEN
- GITLAB_CI_API_TOKEN: $QA_GITLAB_CI_TOKEN
- when: always
- script:
- - |
- bundle exec gitlab-qa-report \
- --generate-test-session "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json" \
- --project "$QA_TESTCASE_SESSIONS_PROJECT"
- artifacts:
- when: always
- expire_in: 1d
- paths:
- - qa/REPORT_ISSUE_URL
notify-slack:
extends:
- - .notify-slack-qa
- - .qa-install
- - .ruby-image
+ - .notify-slack
- .rules:report:process-results
- stage: notify
variables:
- ALLURE_JOB_NAME: e2e-package-and-test
- SLACK_ICON_EMOJI: ci_failing
- STATUS_SYM: ☠️
- STATUS: failed
TYPE: "(package-and-test) "
- when: always
- script:
- - |
- if [ "$SUITE_FAILED" != "true" ] && [ "$SUITE_RAN" == "true" ]; then
- echo "Test suite passed. Exiting..."
- exit 0
- fi
- - bundle exec gitlab-qa-report --prepare-stage-reports "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.xml" # generate summary
- - !reference [.notify-slack-qa, script]
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
new file mode 100644
index 00000000000..49695cb11af
--- /dev/null
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -0,0 +1,282 @@
+default:
+ interruptible: true
+
+workflow:
+ name: $PIPELINE_NAME
+
+include:
+ - project: gitlab-org/quality/pipeline-common
+ ref: 3.1.5
+ file:
+ - /ci/base.gitlab-ci.yml
+ - /ci/allure-report.yml
+ - /ci/knapsack-report.yml
+
+stages:
+ - test
+ - report
+ - notify
+
+# ==========================================
+# Templates
+# ==========================================
+.parallel:
+ parallel: 5
+ variables:
+ QA_KNAPSACK_REPORT_PATH: $CI_PROJECT_DIR/qa/knapsack
+
+.ruby-image:
+ # Because this pipeline template can be included directly in other projects,
+ # image path and registry needs to be defined explicitly
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3
+
+.qa-install:
+ variables:
+ BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true"
+ BUNDLE_SILENCE_ROOT_WARNING: "true"
+ extends:
+ - .gitlab-qa-install
+
+.update-script:
+ script:
+ - !reference [.bundle-prefix]
+ - export QA_COMMAND="$BUNDLE_PREFIX gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $GITLAB_SEMVER_VERSION $UPDATE_TYPE $UPDATE_FROM_EDITION -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS"
+ - echo "Running - '$QA_COMMAND'"
+ - eval "$QA_COMMAND"
+
+.qa:
+ extends:
+ - .qa-base
+ - .qa-install
+ - .gitlab-qa-report
+ stage: test
+ tags:
+ - e2e
+ variables:
+ QA_GENERATE_ALLURE_REPORT: "true"
+ QA_CAN_TEST_PRAEFECT: "false"
+ QA_INTERCEPT_REQUESTS: "true"
+ GITLAB_LICENSE_MODE: test
+ GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
+ GITLAB_QA_OPTS: $EXTRA_GITLAB_QA_OPTS
+ # todo: remove in 16.1 milestone when not needed for backwards compatibility anymore
+ EE_LICENSE: $QA_EE_LICENSE
+ GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN
+ # Allow QA jobs to fail as they are flaky. The top level `package-and-e2e:ee`
+ # pipeline is not allowed to fail, so without allowing QA to fail, we will be
+ # blocking merges due to flaky tests.
+ allow_failure: true
+
+.trigger-omnibus-env:
+ stage: .pre
+ needs:
+ # We need this job because we need its `cached-assets-hash.txt` artifact, so that we can pass the assets image tag to the downstream omnibus-gitlab pipeline.
+ - pipeline: $PARENT_PIPELINE_ID
+ job: build-assets-image
+ variables:
+ BUILD_ENV: build.env
+ before_script:
+ - |
+ # This is duplicating the function from `scripts/utils.sh` since `.gitlab/ci/package-and-test/main.gitlab-ci.yml` can be included in other projects.
+ function assets_image_tag() {
+ local cache_assets_hash_file="cached-assets-hash.txt"
+
+ if [[ -n "${CI_COMMIT_TAG}" ]]; then
+ echo -n "${CI_COMMIT_REF_NAME}"
+ elif [[ -f "${cache_assets_hash_file}" ]]; then
+ echo -n "assets-hash-$(cat ${cache_assets_hash_file} | cut -c1-10)"
+ else
+ echo -n "${CI_COMMIT_SHA}"
+ fi
+ }
+ script:
+ - |
+ SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true")
+ echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV
+ echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV
+ for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done
+ echo "OMNIBUS_GITLAB_RUBY3_BUILD=${OMNIBUS_GITLAB_RUBY3_BUILD:-false}" >> $BUILD_ENV
+ echo "OMNIBUS_GITLAB_RUBY2_BUILD=${OMNIBUS_GITLAB_RUBY2_BUILD:-false}" >> $BUILD_ENV
+ echo "OMNIBUS_GITLAB_CACHE_EDITION=${OMNIBUS_GITLAB_CACHE_EDITION:-GITLAB}" >> $BUILD_ENV
+ echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV
+ echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV
+ echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV
+ target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}"
+ echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV
+ - |
+ echo "Built environment file for omnibus build:"
+ cat $BUILD_ENV
+ artifacts:
+ expire_in: 3 days
+ reports:
+ dotenv: $BUILD_ENV
+ paths:
+ - $BUILD_ENV
+
+.trigger-omnibus-env-ce:
+ extends: .trigger-omnibus-env
+ needs:
+ - pipeline: $PARENT_PIPELINE_ID
+ job: build-assets-image as-if-foss
+
+.trigger-omnibus:
+ stage: .pre
+ inherit:
+ variables: false
+ variables:
+ GITALY_SERVER_VERSION: $GITALY_SERVER_VERSION
+ GITLAB_ELASTICSEARCH_INDEXER_VERSION: $GITLAB_ELASTICSEARCH_INDEXER_VERSION
+ GITLAB_KAS_VERSION: $GITLAB_KAS_VERSION
+ GITLAB_METRICS_EXPORTER_VERSION: $GITLAB_METRICS_EXPORTER_VERSION
+ GITLAB_PAGES_VERSION: $GITLAB_PAGES_VERSION
+ GITLAB_SHELL_VERSION: $GITLAB_SHELL_VERSION
+ GITLAB_WORKHORSE_VERSION: $GITLAB_WORKHORSE_VERSION
+ GITLAB_VERSION: $CI_COMMIT_SHA
+ GITLAB_ASSETS_TAG: $GITLAB_ASSETS_TAG
+ IMAGE_TAG: $CI_COMMIT_SHA
+ TOP_UPSTREAM_SOURCE_PROJECT: $CI_PROJECT_PATH
+ SECURITY_SOURCES: $SECURITY_SOURCES
+ CACHE_UPDATE: $OMNIBUS_GITLAB_CACHE_UPDATE
+ RUBY3_BUILD: $OMNIBUS_GITLAB_RUBY3_BUILD
+ RUBY2_BUILD: $OMNIBUS_GITLAB_RUBY2_BUILD
+ CACHE_EDITION: $OMNIBUS_GITLAB_CACHE_EDITION
+ BUILD_ON_ALL_OS: $OMNIBUS_GITLAB_BUILD_ON_ALL_OS
+ SKIP_QA_TEST: "true"
+ ee: $EE
+ trigger:
+ project: gitlab-org/build/omnibus-gitlab-mirror
+ branch: $TRIGGER_BRANCH
+ strategy: depend
+
+.trigger-omnibus-ce:
+ extends:
+ - .trigger-omnibus
+ variables:
+ # Override gitlab repository so that omnibus doesn't use foss repository for CE build
+ GITLAB_ALTERNATIVE_REPO: $CI_PROJECT_URL
+
+.download-knapsack-report:
+ extends:
+ - .gitlab-qa-image
+ stage: .pre
+ variables:
+ KNAPSACK_DIR: ${CI_PROJECT_DIR}/qa/knapsack
+ GIT_STRATEGY: none
+ script:
+ # when using qa-image, code runs in /home/gitlab/qa folder
+ - bundle exec rake "knapsack:download[test]"
+ - mkdir -p "$KNAPSACK_DIR" && cp knapsack/*.json "${KNAPSACK_DIR}/"
+ allow_failure: true
+ artifacts:
+ paths:
+ - qa/knapsack/*.json
+ expire_in: 1 day
+
+.e2e-test-report:
+ extends:
+ - .generate-allure-report-base
+ stage: report
+ variables:
+ GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
+ ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
+ ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
+
+.upload-knapsack-report:
+ extends:
+ - .generate-knapsack-report-base
+ - .qa-install
+ - .ruby-image
+ stage: report
+ when: always
+
+.export-test-metrics:
+ extends:
+ - .qa-install
+ - .ruby-image
+ stage: report
+ when: always
+ variables:
+ QA_METRICS_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/gitlab-qa-run-*/**/test-metrics-*.json
+ script:
+ - bundle exec rake "ci:export_test_metrics[$QA_METRICS_REPORT_FILE_PATTERN]"
+
+.relate-test-failures:
+ extends:
+ - .qa-install
+ - .ruby-image
+ stage: report
+ variables:
+ QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
+ QA_FAILURES_MAX_DIFF_RATIO: "0.15"
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json
+ GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN
+ when: always
+ script:
+ - |
+ if [ "$SUITE_FAILED" != "true" ] && [ "$SUITE_RAN" == "true" ]; then
+ echo "Test suite passed. Exiting..."
+ exit 0
+ fi
+ - |
+ bundle exec gitlab-qa-report \
+ --relate-failure-issue "$QA_RSPEC_JSON_FILE_PATTERN" \
+ --project "$QA_FAILURES_REPORTING_PROJECT" \
+ --max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO"
+
+.generate-test-session:
+ extends:
+ - .qa-install
+ - .ruby-image
+ stage: report
+ variables:
+ QA_TESTCASE_SESSIONS_PROJECT: gitlab-org/quality/testcase-sessions
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json
+ GITLAB_QA_ACCESS_TOKEN: $QA_TEST_SESSION_TOKEN
+ GITLAB_CI_API_TOKEN: $QA_GITLAB_CI_TOKEN
+ when: always
+ script:
+ - |
+ bundle exec gitlab-qa-report \
+ --generate-test-session "$QA_RSPEC_JSON_FILE_PATTERN" \
+ --project "$QA_TESTCASE_SESSIONS_PROJECT"
+ artifacts:
+ when: always
+ expire_in: 1d
+ paths:
+ - qa/REPORT_ISSUE_URL
+
+.notify-slack:
+ extends:
+ - .notify-slack-qa
+ - .qa-install
+ - .ruby-image
+ stage: notify
+ variables:
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json
+ SLACK_ICON_EMOJI: ci_failing
+ STATUS_SYM: ☠️
+ STATUS: failed
+ when: always
+ script:
+ - |
+ if [ "$SUITE_FAILED" != "true" ] && [ "$SUITE_RAN" == "true" ]; then
+ echo "Test suite passed. Exiting..."
+ exit 0
+ fi
+ - bundle exec gitlab-qa-report --prepare-stage-reports "$QA_RSPEC_JSON_FILE_PATTERN" # generate summary
+ - !reference [.notify-slack-qa, script]
+
+# ==========================================
+# Pre stage
+# ==========================================
+dont-interrupt-me:
+ stage: .pre
+ interruptible: false
+ script:
+ - echo "This jobs makes sure this pipeline won't be interrupted! See https://docs.gitlab.com/ee/ci/yaml/#interruptible."
+ rules:
+ - if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $CI_MERGE_REQUEST_IID == null'
+ allow_failure: true
+ - if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
+ when: manual
+ allow_failure: true
diff --git a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
index bb22ea41b47..3580339921d 100644
--- a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
@@ -48,14 +48,6 @@
rules:
- when: always
-.rules:dont-interrupt:
- rules:
- - if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $CI_MERGE_REQUEST_IID == null'
- allow_failure: true
- - if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
- when: manual
- allow_failure: true
-
.rules:omnibus-build:
rules:
- if: $SKIP_OMNIBUS_TRIGGER == "true"
diff --git a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
index 0322247a89d..0322247a89d 100644
--- a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index dae96808a61..97a2883c9e8 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -12,6 +12,40 @@
- !reference [.default-before_script, before_script]
- cd qa && bundle install
+.e2e-trigger-base:
+ extends: .production # this makes sure GITLAB_ALLOW_SEPARATE_CI_DATABASE is passed to the child pipeline
+ stage: qa
+ needs:
+ - build-assets-image
+ - build-qa-image
+ - e2e-test-pipeline-generate
+ variables:
+ # This is needed by `trigger-omnibus-env` (`.gitlab/ci/package-and-test/main.gitlab-ci.yml`).
+ PARENT_PIPELINE_ID: $CI_PIPELINE_ID
+ SKIP_MESSAGE: Skipping package-and-test due to mr containing only quarantine changes!
+ GITLAB_QA_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-ee-qa:${CI_COMMIT_SHA}"
+ RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
+ QA_PATH: qa # sets the optional path for bundler to run from
+ DYNAMIC_PIPELINE_YML: package-and-test-pipeline.yml # yml files are generated by scripts/generate-e2e-pipeline script
+ inherit:
+ variables:
+ - CHROME_VERSION
+ - RUBY_VERSION
+ - DOCKER_VERSION
+ - REGISTRY_GROUP
+ - REGISTRY_HOST
+ - OMNIBUS_GITLAB_CACHE_EDITION
+ - OMNIBUS_GITLAB_RUBY3_BUILD
+ - OMNIBUS_GITLAB_RUBY2_BUILD
+ trigger:
+ strategy: depend
+ forward:
+ yaml_variables: true
+ pipeline_variables: true
+ include:
+ - artifact: $DYNAMIC_PIPELINE_YML
+ job: e2e-test-pipeline-generate
+
qa:internal:
extends:
- .qa-job-base
@@ -54,42 +88,17 @@ qa:update-qa-cache:
e2e:package-and-test-ee:
extends:
- - .production # this makes sure GITLAB_ALLOW_SEPARATE_CI_DATABASE is passed to the child pipeline
+ - .e2e-trigger-base
- .qa:rules:package-and-test-ee
- stage: qa
needs:
- build-assets-image
- build-qa-image
- e2e-test-pipeline-generate
variables:
- # This is needed by `trigger-omnibus-env` (`.gitlab/ci/package-and-test/main.gitlab-ci.yml`).
- PARENT_PIPELINE_ID: $CI_PIPELINE_ID
- SKIP_MESSAGE: Skipping package-and-test due to mr containing only quarantine changes!
RELEASE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}"
- GITLAB_QA_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-ee-qa:${CI_COMMIT_SHA}"
- RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
- QA_PATH: qa # sets the optional path for bundler to run from
QA_RUN_TYPE: e2e-package-and-test
ALLURE_JOB_NAME: e2e-package-and-test
PIPELINE_NAME: E2E Omnibus GitLab EE
- inherit:
- variables:
- - CHROME_VERSION
- - RUBY_VERSION
- - DOCKER_VERSION
- - REGISTRY_GROUP
- - REGISTRY_HOST
- - OMNIBUS_GITLAB_CACHE_EDITION
- - OMNIBUS_GITLAB_RUBY3_BUILD
- - OMNIBUS_GITLAB_RUBY2_BUILD
- trigger:
- strategy: depend
- forward:
- yaml_variables: true
- pipeline_variables: true
- include:
- - artifact: package-and-test-pipeline.yml
- job: e2e-test-pipeline-generate
e2e:package-and-test-ce:
extends:
@@ -119,8 +128,26 @@ e2e:package-and-test-super-sidebar:
ALLURE_JOB_NAME: e2e-package-and-test-super-sidebar
PIPELINE_NAME: E2E Omnibus Super Sidebar
+e2e:package-and-test-nightly:
+ extends:
+ - .e2e-trigger-base
+ - .qa:rules:package-and-test-nightly
+ needs:
+ - build-assets-image
+ - build-assets-image as-if-foss
+ - build-qa-image
+ - build-qa-image as-if-foss
+ - e2e-test-pipeline-generate
+ variables:
+ GITLAB_SEMVER_VERSION: $GITLAB_SEMVER_VERSION
+ QA_RUN_TYPE: nightly
+ ALLURE_JOB_NAME: nightly
+ PIPELINE_NAME: E2E Omnibus GitLab Nightly
+ DYNAMIC_PIPELINE_YML: package-and-test-nightly-pipeline.yml
+
e2e:test-on-gdk:
extends:
+ - .e2e-trigger-base
- .qa:rules:e2e:test-on-gdk
stage: qa
needs:
@@ -128,12 +155,12 @@ e2e:test-on-gdk:
# In MRs we assume the last scheduled master pipeline built the image already.
- job: build-qa-on-gdk-master-image
optional: true
+ - job: e2e-test-pipeline-generate
+ artifacts: true
variables:
- QA_RUN_TYPE: e2e-test-on-gdk # Setting it here so that all the child pipeline reporting jobs inherit this variable
+ ALLURE_JOB_NAME: e2e-test-on-gdk
+ QA_RUN_TYPE: e2e-test-on-gdk
+ PIPELINE_NAME: E2E GDK
+ DYNAMIC_PIPELINE_YML: test-on-gdk-pipeline.yml
+ SKIP_MESSAGE: Skipping test-on-gdk due to mr containing only quarantine changes!
allow_failure: true
- trigger:
- strategy: depend
- forward:
- yaml_variables: true
- pipeline_variables: true
- include: .gitlab/ci/test-on-gdk/main.gitlab-ci.yml
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 82f46df80a7..0e158fff653 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1477,7 +1477,16 @@
KNAPSACK_GENERATE_REPORT: "false"
UPDATE_QA_CACHE: "false"
QA_SAVE_TEST_METRICS: "true"
- QA_EXPORT_TEST_METRICS: "false" # on main runs, metrics are exported to separate bucket via rake task for better consistency
+ QA_EXPORT_TEST_METRICS: "false"
+
+.qa:rules:package-and-test-nightly:
+ rules:
+ - <<: *if-default-branch-schedule-nightly
+ variables:
+ KNAPSACK_GENERATE_REPORT: "true"
+ SKIP_REPORT_IN_ISSUES: "false"
+ QA_SAVE_TEST_METRICS: "true"
+ QA_EXPORT_TEST_METRICS: "false"
###############
# Rails rules #
@@ -1576,6 +1585,8 @@
.rails:rules:rspec-predictive:
rules:
+ - <<: *if-fork-merge-request
+ changes: *code-backstage-patterns
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-approved
@@ -1655,6 +1666,8 @@
rules:
- <<: *if-not-ee
when: never
+ - <<: *if-fork-merge-request
+ when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-labels-run-all-rspec
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index f2d9bbc6ffa..8e80598bc78 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -163,8 +163,6 @@ e2e-test-pipeline-generate:
stage: prepare
variables:
ENV_FILE: $CI_PROJECT_DIR/qa_tests_vars.env
- OMNIBUS_PIPELINE_YML: package-and-test-pipeline.yml
- REVIEW_PIPELINE_YML: review-app-pipeline.yml
COLORIZED_LOGS: "true"
script:
- bundle exec rake "ci:detect_changes[$ENV_FILE]"
@@ -172,5 +170,4 @@ e2e-test-pipeline-generate:
artifacts:
expire_in: 1 day
paths:
- - $OMNIBUS_PIPELINE_YML
- - $REVIEW_PIPELINE_YML
+ - '*-pipeline.yml'
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index 10eca154b31..6eab87bd84c 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -1,55 +1,7 @@
-default:
- interruptible: true
-
include:
- - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
- - project: 'gitlab-org/quality/pipeline-common'
- ref: 3.1.3
- file:
- - /ci/base.gitlab-ci.yml
- - /ci/allure-report.yml
- - /ci/knapsack-report.yml
-
-stages:
- - test
- - report
- - notify
-
-.qa-install:
- variables:
- BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true"
- BUNDLE_SILENCE_ROOT_WARNING: "true"
- RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
- QA_PATH: qa # sets the optional path for bundler to run from
- extends:
- - .gitlab-qa-install
-
-dont-interrupt-me:
- extends: .rules:dont-interrupt
- stage: test
- interruptible: false
- script:
- - echo "This jobs makes sure this pipeline won't be interrupted! See https://docs.gitlab.com/ee/ci/yaml/#interruptible."
-
-download-knapsack-report:
- extends:
- - .gitlab-qa-image
- - .rules:download-knapsack
- stage: .pre
- variables:
- KNAPSACK_DIR: ${CI_PROJECT_DIR}/qa/knapsack
- GIT_STRATEGY: none
- script:
- # when using qa-image, code runs in /home/gitlab/qa folder
- - bundle exec rake "knapsack:download[test]"
- - mkdir -p "$KNAPSACK_DIR" && cp knapsack/*.json "${KNAPSACK_DIR}/"
- - echo "$PROCESS_TEST_RESULTS"
- allow_failure: true
- artifacts:
- paths:
- - qa/knapsack/*.json
- expire_in: 1 day
+ - local: .gitlab/ci/qa-common/main.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml
+ - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml
.run-tests:
stage: test
@@ -72,7 +24,6 @@ download-knapsack-report:
EE_LICENSE: $QA_EE_LICENSE
GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
- QA_KNAPSACK_REPORTS: qa-smoke,ee-instance-parallel
RSPEC_REPORT_OPTS: "--format QA::Support::JsonFormatter --out tmp/rspec-${CI_JOB_ID}.json --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec-${CI_JOB_ID}.htm --color --format documentation"
timeout: 2 hours
artifacts:
@@ -99,17 +50,22 @@ download-knapsack-report:
--volume $CI_PROJECT_DIR/test_output:/home/gdk/gdk/gitlab/qa/tmp:z \
--volume $CI_PROJECT_DIR/logs/gdk:/home/gdk/gdk/log \
--volume $CI_PROJECT_DIR/logs/gitlab:/home/gdk/gdk/gitlab/log \
+ --volume $CI_PROJECT_DIR/qa/knapsack:/home/gdk/gdk/gitlab/qa/knapsack \
${QA_GDK_IMAGE} "${CI_COMMIT_SHA}" "$RSPEC_REPORT_OPTS $TEST_GDK_TAGS --tag ~requires_praefect"
# The above image's launch script takes two arguments only - first one is the commit sha and the second one Rspec Args
allow_failure: true
+download-knapsack-report:
+ extends:
+ - .download-knapsack-report
+ - .rules:download-knapsack
+
test-on-gdk-smoke:
extends:
- .run-tests
parallel: 2
variables:
TEST_GDK_TAGS: "--tag smoke"
- QA_KNAPSACK_REPORT_NAME: qa-smoke
rules:
- when: always
@@ -117,8 +73,6 @@ test-on-gdk-full:
extends:
- .run-tests
parallel: 5
- variables:
- QA_KNAPSACK_REPORT_NAME: ee-instance-parallel
rules:
- when: manual
@@ -127,92 +81,43 @@ test-on-gdk-full:
# ==========================================
e2e-test-report:
extends:
- - .generate-allure-report-base
+ - .e2e-test-report
- .rules:report:allure-report
- stage: report
variables:
- ALLURE_JOB_NAME: e2e-test-on-gdk
ALLURE_RESULTS_GLOB: test_output/allure-results
- GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
- ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
- ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
upload-knapsack-report:
extends:
- - .generate-knapsack-report-base
- - .qa-install
- - .ruby-qa-image
+ - .upload-knapsack-report
- .rules:report:process-results
variables:
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/test_output/knapsack/*/*.json
- stage: report
- when: always
export-test-metrics:
extends:
- - .qa-install
- - .ruby-qa-image
+ - .export-test-metrics
- .rules:report:process-results
- stage: report
- when: always
- script:
- - pwd
- - bundle exec rake "ci:export_test_metrics[$CI_PROJECT_DIR/test_output/test-metrics-*.json]"
+ variables:
+ QA_METRICS_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/test_output/test-metrics-*.json
relate-test-failures:
extends:
- - .qa-install
- - .ruby-qa-image
+ - .relate-test-failures
- .rules:report:process-results
- stage: report
variables:
- QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
- QA_FAILURES_MAX_DIFF_RATIO: "0.15"
- GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN
- when: on_failure
- script:
- - |
- bundle exec gitlab-qa-report \
- --relate-failure-issue "$CI_PROJECT_DIR/test_output/rspec-*.json" \
- --project "$QA_FAILURES_REPORTING_PROJECT" \
- --max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO"
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/test_output/rspec-*.json
generate-test-session:
extends:
- - .qa-install
- - .ruby-qa-image
+ - .generate-test-session
- .rules:report:process-results
- stage: report
variables:
- QA_TESTCASE_SESSIONS_PROJECT: gitlab-org/quality/testcase-sessions
- GITLAB_QA_ACCESS_TOKEN: $QA_TEST_SESSION_TOKEN
- GITLAB_CI_API_TOKEN: $QA_GITLAB_CI_TOKEN
- when: always
- script:
- - |
- bundle exec gitlab-qa-report \
- --generate-test-session "$CI_PROJECT_DIR/test_output/rspec-*.json" \
- --project "$QA_TESTCASE_SESSIONS_PROJECT"
- artifacts:
- when: always
- expire_in: 1d
- paths:
- - qa/REPORT_ISSUE_URL
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/test_output/rspec-*.json
notify-slack:
extends:
- - .notify-slack-qa
- - .qa-install
- - .ruby-qa-image
+ - .notify-slack
- .rules:report:process-results
- stage: notify
variables:
- ALLURE_JOB_NAME: e2e-test-on-gdk
- SLACK_ICON_EMOJI: ci_failing
- STATUS_SYM: ☠️
- STATUS: failed
- TYPE: "(e2e-test-on-gdk) "
- when: on_failure
- script:
- - bundle exec gitlab-qa-report --prepare-stage-reports "$CI_PROJECT_DIR/test_output/rspec-*.xml" # generate summary
- - !reference [.notify-slack-qa, script]
+ QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/test_output/rspec-*.json
+ TYPE: "(gdk) "
diff --git a/.rubocop_todo/rspec/shared_groups_metadata.yml b/.rubocop_todo/rspec/shared_groups_metadata.yml
index 5b1ec2dbf5c..ddac8bf6589 100644
--- a/.rubocop_todo/rspec/shared_groups_metadata.yml
+++ b/.rubocop_todo/rspec/shared_groups_metadata.yml
@@ -4,4 +4,3 @@ RSpec/SharedGroupsMetadata:
Exclude:
- 'spec/lib/gitlab/ci/config/entry/retry_spec.rb'
- 'spec/lib/gitlab/git/merge_base_spec.rb'
- - 'spec/models/container_repository_spec.rb'
diff --git a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
index 0c5bd61d580..d8d16220d5f 100644
--- a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
+++ b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
@@ -271,8 +271,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/run_pipeline_schedule_worker.rb'
- 'app/workers/schedule_merge_request_cleanup_refs_worker.rb'
- 'app/workers/schedule_migrate_external_diffs_worker.rb'
- - 'app/workers/self_monitoring_project_create_worker.rb'
- - 'app/workers/self_monitoring_project_delete_worker.rb'
- 'app/workers/service_desk_email_receiver_worker.rb'
- 'app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb'
- 'app/workers/ssh_keys/expired_notification_worker.rb'
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index 6aa5bb715b2..d2db61e096a 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -117,7 +117,7 @@ export default {
{{ __('Summary comment (optional)') }}
</template>
<div class="common-note-form gfm-form">
- <div class="comment-warning-wrapper-large gl-border-0 gl-bg-white">
+ <div class="comment-warning-wrapper-large gl-border-0 gl-bg-white gl-overflow-hidden">
<markdown-field
:is-submitting="isSubmitting"
:add-spacing-classes="false"
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
index fd696670ddf..7c5cc1ea6ee 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
@@ -1,11 +1,13 @@
<script>
import {
GlLink,
+ GlSprintf,
GlForm,
GlFormGroup,
GlFormInput,
GlButton,
GlButtonGroup,
+ GlLoadingIcon,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { getMarkType, getMarkRange } from '@tiptap/core';
@@ -16,12 +18,14 @@ import BubbleMenu from './bubble_menu.vue';
export default {
components: {
BubbleMenu,
+ GlSprintf,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlButton,
GlButtonGroup,
+ GlLoadingIcon,
EditorStateObserver,
},
directives: {
@@ -35,6 +39,9 @@ export default {
linkText: undefined,
isEditing: false,
+
+ uploading: false,
+ uploadProgress: 0,
};
},
methods: {
@@ -129,9 +136,11 @@ export default {
updateLinkToState() {
const editor = this.tiptapEditor;
- const { href, canonicalSrc } = editor.getAttributes(Link.name);
+ const { href, canonicalSrc, uploading } = editor.getAttributes(Link.name);
const text = this.linkTextInDoc();
+ this.uploading = uploading;
+
if (
canonicalSrc === this.linkCanonicalSrc &&
href === this.linkHref &&
@@ -150,6 +159,11 @@ export default {
if (transaction.getMeta('creatingLink')) {
this.isEditing = true;
}
+
+ const { filename = '', progress = 0 } = transaction.getMeta('uploadProgress') || {};
+ if (this.uploading === filename) {
+ this.uploadProgress = Math.round(progress * 100);
+ }
},
copyLinkHref() {
@@ -203,7 +217,14 @@ export default {
@hidden="resetBubbleMenuState"
>
<gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
+ <gl-loading-icon v-if="uploading" class="gl-pl-4 gl-pr-3" />
+ <span v-if="uploading" class="gl-text-secondary gl-pr-3">
+ <gl-sprintf :message="__('Uploading: %{progress}')">
+ <template #progress>{{ uploadProgress }}&percnt;</template>
+ </gl-sprintf>
+ </span>
<gl-link
+ v-else
v-gl-tooltip
:href="linkHref"
:aria-label="linkCanonicalSrc"
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
index 1bfa635c03b..6bb6bdc4e65 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
@@ -1,6 +1,7 @@
<script>
import {
GlLink,
+ GlSprintf,
GlForm,
GlFormGroup,
GlFormInput,
@@ -48,6 +49,7 @@ export default {
},
components: {
BubbleMenu,
+ GlSprintf,
GlForm,
GlFormGroup,
GlFormInput,
@@ -71,7 +73,10 @@ export default {
isEditing: false,
isUpdating: false,
- isUploading: false,
+
+ uploading: false,
+
+ uploadProgress: 0,
};
},
computed: {
@@ -88,7 +93,7 @@ export default {
return this.$options.i18n.deleteLabels[this.mediaType];
},
showProgressIndicator() {
- return this.isUploading || this.isUpdating;
+ return this.uploading || this.isUpdating;
},
isDrawioDiagram() {
return this.mediaType === DrawioDiagram.name;
@@ -157,17 +162,27 @@ export default {
this.mediaTitle = title;
this.mediaAlt = alt;
this.mediaCanonicalSrc = canonicalSrc || src;
- this.isUploading = uploading;
+ this.uploading = uploading;
+
this.mediaSrc = await this.contentEditor.resolveUrl(this.mediaCanonicalSrc);
this.isUpdating = false;
},
+ onTransaction({ transaction }) {
+ const { filename = '', progress = 0 } = transaction.getMeta('uploadProgress') || {};
+ if (this.uploading === filename) {
+ this.uploadProgress = Math.round(progress * 100);
+ }
+ },
+
resetMediaInfo() {
this.mediaTitle = null;
this.mediaAlt = null;
this.mediaCanonicalSrc = null;
- this.isUploading = false;
+ this.uploading = false;
+
+ this.uploadProgress = 0;
},
replaceMedia() {
@@ -204,17 +219,26 @@ export default {
};
</script>
<template>
- <bubble-menu
- data-testid="media-bubble-menu"
- class="gl-shadow gl-rounded-base gl-bg-white"
- plugin-key="bubbleMenuMedia"
- :should-show="shouldShow"
- @show="updateMediaInfoToState"
- @hidden="resetMediaInfo"
+ <editor-state-observer
+ :debounce="0"
+ @selectionUpdate="updateMediaInfoToState"
+ @transaction="onTransaction"
>
- <editor-state-observer :debounce="0" @transaction="updateMediaInfoToState">
+ <bubble-menu
+ data-testid="media-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ plugin-key="bubbleMenuMedia"
+ :should-show="shouldShow"
+ @show="updateMediaInfoToState"
+ @hidden="resetMediaInfo"
+ >
<gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
<gl-loading-icon v-if="showProgressIndicator" class="gl-pl-4 gl-pr-3" />
+ <span v-if="uploading" class="gl-text-secondary gl-pr-3">
+ <gl-sprintf :message="__('Uploading: %{progress}')">
+ <template #progress>{{ uploadProgress }}&percnt;</template>
+ </gl-sprintf>
+ </span>
<input
ref="fileSelector"
type="file"
@@ -280,7 +304,7 @@ export default {
data-testid="replace-media"
:aria-label="replaceLabel"
:title="replaceLabel"
- icon="upload"
+ icon="retry"
@click="replaceMedia"
/>
<gl-button
@@ -315,6 +339,6 @@ export default {
<gl-button variant="confirm" type="submit">{{ __('Apply') }}</gl-button>
</div>
</gl-form>
- </editor-state-observer>
- </bubble-menu>
+ </bubble-menu>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 2b2c4a5ac1c..7fee798c65a 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -220,7 +220,7 @@ export default {
<div
data-testid="content-editor"
data-qa-selector="content_editor_container"
- class="md-area"
+ class="md-area gl-border-none! gl-shadow-none!"
:class="{ 'is-focused': focused }"
>
<formatting-toolbar ref="toolbar" @enableMarkdownEditor="$emit('enableMarkdownEditor')" />
diff --git a/app/assets/javascripts/content_editor/components/content_editor_alert.vue b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
index 87eff2451ec..59d71169dd3 100644
--- a/app/assets/javascripts/content_editor/components/content_editor_alert.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
@@ -34,7 +34,6 @@ export default {
<editor-state-observer @alert="displayAlert">
<gl-alert
v-if="message"
- class="gl-mb-6"
:variant="variant"
:primary-button-text="actionLabel"
@dismiss="dismissAlert"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
index 1f18090e7d7..5d98fdeef02 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
@@ -23,13 +23,9 @@ export default {
this.$refs.fileSelector.click();
},
onFileSelect(e) {
- this.tiptapEditor
- .chain()
- .focus()
- .uploadAttachment({
- file: e.target.files[0],
- })
- .run();
+ for (const file of e.target.files) {
+ this.tiptapEditor.chain().focus().uploadAttachment({ file }).run();
+ }
// Reset the file input so that the same file can be uploaded again
this.$refs.fileSelector.value = '';
@@ -53,6 +49,7 @@ export default {
<input
ref="fileSelector"
type="file"
+ multiple
name="content_editor_image"
class="gl-display-none"
data-qa-selector="file_upload_field"
diff --git a/app/assets/javascripts/content_editor/extensions/attachment.js b/app/assets/javascripts/content_editor/extensions/attachment.js
index 0d5b8e56a6c..e7a6af30266 100644
--- a/app/assets/javascripts/content_editor/extensions/attachment.js
+++ b/app/assets/javascripts/content_editor/extensions/attachment.js
@@ -2,6 +2,20 @@ import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { handleFileEvent } from '../services/upload_helpers';
+const processFiles = ({ files, uploadsPath, renderMarkdown, eventHub, editor }) => {
+ if (!files.length) {
+ return false;
+ }
+
+ let handled = true;
+
+ for (const file of files) {
+ handled = handled && handleFileEvent({ editor, file, uploadsPath, renderMarkdown, eventHub });
+ }
+
+ return handled;
+};
+
export default Extension.create({
name: 'attachment',
@@ -36,25 +50,17 @@ export default Extension.create({
key: new PluginKey('attachment'),
props: {
handlePaste: (_, event) => {
- const { uploadsPath, renderMarkdown, eventHub } = this.options;
-
- return handleFileEvent({
+ return processFiles({
+ files: event.clipboardData.files,
editor,
- file: event.clipboardData.files[0],
- uploadsPath,
- renderMarkdown,
- eventHub,
+ ...this.options,
});
},
handleDrop: (_, event) => {
- const { uploadsPath, renderMarkdown, eventHub } = this.options;
-
- return handleFileEvent({
+ return processFiles({
+ files: event.dataTransfer.files,
editor,
- file: event.dataTransfer.files[0],
- uploadsPath,
- renderMarkdown,
- eventHub,
+ ...this.options,
});
},
},
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index 314d5230b01..b83814103d1 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -29,7 +29,6 @@ export default Link.extend({
addInputRules() {
const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
- const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
return [
markInputRule({
@@ -37,16 +36,15 @@ export default Link.extend({
type: this.type,
getAttributes: extractHrefFromMarkdownLink,
}),
- markInputRule({
- find: urlSyntaxRegExp,
- type: this.type,
- getAttributes: extractHrefFromMatch,
- }),
];
},
addAttributes() {
return {
...this.parent?.(),
+ uploading: {
+ default: false,
+ renderHTML: ({ uploading }) => (uploading ? { class: 'with-attachment-icon' } : {}),
+ },
href: {
default: null,
parseHTML: (element) => element.getAttribute('href'),
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
deleted file mode 100644
index 2324e9b132d..00000000000
--- a/app/assets/javascripts/content_editor/extensions/loading.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Node } from '@tiptap/core';
-
-export default Node.create({
- name: 'loading',
- inline: true,
- group: 'inline',
-
- addAttributes() {
- return {
- label: {
- default: null,
- },
- };
- },
-
- renderHTML({ node }) {
- return [
- 'span',
- { class: 'gl-display-inline-flex gl-align-items-center' },
- ['span', { class: 'gl-spinner gl-mx-2' }],
- ['span', { class: 'gl-link' }, node.attrs.label],
- ];
- },
-});
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index 01ffc217894..47766c966a1 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -61,7 +61,11 @@ export default Node.create({
...this.extraElementAttrs,
},
],
- ['a', { href: node.attrs.src }, node.attrs.title || node.attrs.alt || ''],
+ [
+ 'a',
+ { href: node.attrs.src, class: 'with-attachment-icon' },
+ node.attrs.title || node.attrs.alt || '',
+ ],
];
},
});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 1b366136bfa..3958f77745a 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -40,7 +40,6 @@ import InlineDiff from '../extensions/inline_diff';
import Italic from '../extensions/italic';
import Link from '../extensions/link';
import ListItem from '../extensions/list_item';
-import Loading from '../extensions/loading';
import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
@@ -136,7 +135,6 @@ export const createContentEditor = ({
ExternalKeydownHandler.configure({ eventHub }),
Link,
ListItem,
- Loading,
MathInline,
OrderedList,
Paragraph,
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 9ff50b45088..3b77064e903 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -32,7 +32,6 @@ import InlineDiff from '../extensions/inline_diff';
import Italic from '../extensions/italic';
import Link from '../extensions/link';
import ListItem from '../extensions/list_item';
-import Loading from '../extensions/loading';
import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
@@ -195,7 +194,6 @@ const defaultSerializerConfig = {
inline: true,
}),
[ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
- [Loading.name]: () => {},
[OrderedList.name]: preserveUnchanged(renderOrderedList),
[Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
[Reference.name]: renderReference,
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 48367ac42f5..478b87372d7 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -309,15 +309,13 @@ export function renderHardBreak(state, node, parent, index) {
export function renderImage(state, node) {
const { alt, canonicalSrc, src, title, width, height, isReference } = node.attrs;
- let realSrc = canonicalSrc || src || '';
+ const realSrc = canonicalSrc || src || '';
// eslint-disable-next-line @gitlab/require-i18n-strings
- if (realSrc.startsWith('data:')) realSrc = '';
+ if (realSrc.startsWith('data:') || realSrc.startsWith('blob:')) return;
if (isString(src) || isString(canonicalSrc)) {
const quotedTitle = title ? ` ${state.quote(title)}` : '';
- const sourceExpression = isReference
- ? `[${canonicalSrc}]`
- : `(${state.esc(realSrc)}${quotedTitle})`;
+ const sourceExpression = isReference ? `[${canonicalSrc}]` : `(${realSrc}${quotedTitle})`;
const sizeAttributes = [];
if (width) {
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index 548f5cdf19c..f5785397bf0 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -1,7 +1,33 @@
import { VARIANT_DANGER } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import { extractFilename, readFileAsDataURL } from './utils';
+import { __, sprintf } from '~/locale';
+import { bytesToMiB } from '~/lib/utils/number_utils';
+import TappablePromise from '~/lib/utils/tappable_promise';
+import { ALERT_EVENT } from '../constants';
+
+const chain = (editor) => editor.chain().setMeta('preventAutolink', true);
+
+const findUploadedFilePosition = (editor, filename) => {
+ let position;
+
+ editor.view.state.doc.descendants((descendant, pos) => {
+ if (descendant.attrs.uploading === filename) {
+ position = pos;
+ return false;
+ }
+
+ for (const mark of descendant.marks) {
+ if (mark.type.name === 'link' && mark.attrs.uploading === filename) {
+ position = pos + 1;
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ return position;
+};
export const acceptedMimes = {
drawioDiagram: {
@@ -47,6 +73,18 @@ const extractAttachmentLinkUrl = (html) => {
return { src, canonicalSrc };
};
+class UploadError extends Error {}
+
+const notifyUploadError = (eventHub, error) => {
+ eventHub.$emit(ALERT_EVENT, {
+ message:
+ error instanceof UploadError
+ ? error.message
+ : __('An error occurred while uploading the file. Please try again.'),
+ variant: VARIANT_DANGER,
+ });
+};
+
/**
* Uploads a file with a post request to the URL indicated
* in the uploadsPath parameter. The expected response of the
@@ -64,85 +102,147 @@ const extractAttachmentLinkUrl = (html) => {
* and returns a rendered version in HTML format.
* @param {File} params.file The file to upload
*
- * @returns Returns an object with two properties:
+ * @returns {TappablePromise} Returns an object with two properties:
*
* canonicalSrc: The URL as defined in the Markdown
* src: The absolute URL that points to the resource in the server
*/
-export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
- const formData = new FormData();
- formData.append('file', file, file.name);
+export const uploadFile = ({ uploadsPath, renderMarkdown, file }) => {
+ return new TappablePromise(async (tap) => {
+ const maxFileSize = (gon.max_file_size || 10).toFixed(0);
+ const fileSize = bytesToMiB(file.size);
+ if (fileSize > maxFileSize) {
+ throw new UploadError(
+ sprintf(__('File is too big (%{fileSize}MiB). Max filesize: %{maxFileSize}MiB.'), {
+ fileSize: fileSize.toFixed(2),
+ maxFileSize,
+ }),
+ );
+ }
- const { data } = await axios.post(uploadsPath, formData);
- const { markdown } = data.link;
- const rendered = await renderMarkdown(markdown);
+ const formData = new FormData();
+ formData.append('file', file, file.name);
- return extractAttachmentLinkUrl(rendered);
+ const { data } = await axios.post(uploadsPath, formData, {
+ onUploadProgress: (e) => tap(e.loaded / e.total),
+ });
+ const { markdown } = data.link;
+ const rendered = await renderMarkdown(markdown);
+
+ return extractAttachmentLinkUrl(rendered);
+ });
};
-const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown, eventHub }) => {
- const encodedSrc = await readFileAsDataURL(file);
- const { view } = editor;
+const uploadMedia = async ({ type, editor, file, uploadsPath, renderMarkdown, eventHub }) => {
+ // needed to avoid mismatched transaction error
+ await Promise.resolve();
+
+ const objectUrl = URL.createObjectURL(file);
+ const { selection } = editor.view.state;
+ const currentNode = selection.$to.node();
+
+ let position = selection.to;
+ let content = {
+ type,
+ attrs: { uploading: file.name, src: objectUrl, alt: file.name },
+ };
+ let selectionIncrement = 0;
+
+ // if the current node is not empty, we need to wrap the content in a new paragraph
+ if (currentNode.content.size > 0 || currentNode.type.name === 'doc') {
+ content = {
+ type: 'paragraph',
+ content: [content],
+ };
+ selectionIncrement = 1;
+ }
- editor.commands.insertContent({ type, attrs: { uploading: true, src: encodedSrc } });
+ chain(editor)
+ .insertContentAt(position, content)
+ .setNodeSelection(position + selectionIncrement)
+ .run();
- const { state } = view;
- const position = state.selection.from - 1;
- const { tr } = state;
+ uploadFile({ file, uploadsPath, renderMarkdown })
+ .tap((progress) => {
+ chain(editor).setMeta('uploadProgress', { filename: file.name, progress }).run();
+ })
+ .then(({ canonicalSrc }) => {
+ // the position might have changed while uploading, so we need to find it again
+ position = findUploadedFilePosition(editor, file.name);
- editor.commands.setNodeSelection(position);
+ editor.view.dispatch(
+ editor.state.tr.setMeta('preventAutolink', true).setNodeMarkup(position, undefined, {
+ uploading: false,
+ src: objectUrl,
+ alt: file.name,
+ canonicalSrc,
+ }),
+ );
- try {
- const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
+ chain(editor).setNodeSelection(position).run();
+ })
+ .catch((e) => {
+ position = findUploadedFilePosition(editor, file.name);
- view.dispatch(
- tr.setNodeMarkup(position, undefined, {
- uploading: false,
- src: encodedSrc,
- alt: extractFilename(src),
- canonicalSrc,
- }),
- );
+ chain(editor)
+ .deleteRange({ from: position, to: position + 1 })
+ .run();
- editor.commands.setNodeSelection(position);
- } catch (e) {
- editor.commands.deleteRange({ from: position, to: position + 1 });
- eventHub.$emit('alert', {
- message: __('An error occurred while uploading the file. Please try again.'),
- variant: VARIANT_DANGER,
+ notifyUploadError(eventHub, e);
});
- }
};
const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
+ // needed to avoid mismatched transaction error
await Promise.resolve();
- const { view } = editor;
+ const objectUrl = URL.createObjectURL(file);
+ const { selection } = editor.view.state;
+ const currentNode = selection.$to.node();
+
+ let position = selection.to;
+ let content = {
+ type: 'text',
+ text: file.name,
+ marks: [{ type: 'link', attrs: { href: objectUrl, uploading: file.name } }],
+ };
- const text = extractFilename(file.name);
+ // if the current node is not empty, we need to wrap the content in a new paragraph
+ if (currentNode.content.size > 0 || currentNode.type.name === 'doc') {
+ content = {
+ type: 'paragraph',
+ content: [content],
+ };
+ }
- const { state } = view;
- const { from } = state.selection;
+ chain(editor).insertContentAt(position, content).extendMarkRange('link').run();
- editor.commands.insertContent({
- type: 'loading',
- attrs: { label: text },
- });
+ uploadFile({ file, uploadsPath, renderMarkdown })
+ .tap((progress) => {
+ chain(editor).setMeta('uploadProgress', { filename: file.name, progress }).run();
+ })
+ .then(({ src, canonicalSrc }) => {
+ // the position might have changed while uploading, so we need to find it again
+ position = findUploadedFilePosition(editor, file.name);
- try {
- const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
-
- editor.commands.insertContentAt(
- { from, to: from + 1 },
- { type: 'text', text, marks: [{ type: 'link', attrs: { href: src, canonicalSrc } }] },
- );
- } catch (e) {
- editor.commands.deleteRange({ from, to: from + 1 });
- eventHub.$emit('alert', {
- message: __('An error occurred while uploading the file. Please try again.'),
- variant: VARIANT_DANGER,
+ chain(editor)
+ .setTextSelection(position)
+ .extendMarkRange('link')
+ .updateAttributes('link', { href: src, canonicalSrc, uploading: false })
+ .run();
+ })
+ .catch((e) => {
+ position = findUploadedFilePosition(editor, file.name);
+
+ chain(editor)
+ .setTextSelection(position)
+ .extendMarkRange('link')
+ .unsetLink()
+ .deleteSelection()
+ .run();
+
+ notifyUploadError(eventHub, e);
});
- }
};
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
@@ -150,7 +250,7 @@ export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eve
for (const [type, { mimes, ext }] of Object.entries(acceptedMimes)) {
if (mimes.includes(file?.type) && (!ext || file?.name.endsWith(ext))) {
- uploadContent({ type, editor, file, uploadsPath, renderMarkdown, eventHub });
+ uploadMedia({ type, editor, file, uploadsPath, renderMarkdown, eventHub });
return true;
}
diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js
index e352fa8a9db..1c128b4aa19 100644
--- a/app/assets/javascripts/content_editor/services/utils.js
+++ b/app/assets/javascripts/content_editor/services/utils.js
@@ -4,26 +4,4 @@ export const hasSelection = (tiptapEditor) => {
return from < to;
};
-/**
- * Extracts filename from a URL
- *
- * @example
- * > extractFilename('https://gitlab.com/images/logo-full.png')
- * < 'logo-full'
- *
- * @param {string} src The URL to extract filename from
- * @returns {string}
- */
-export const extractFilename = (src) => {
- return src.replace(/^.*\/|\.[^.]+?$/g, '');
-};
-
-export const readFileAsDataURL = (file) => {
- return new Promise((resolve) => {
- const reader = new FileReader();
- reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
- reader.readAsDataURL(file);
- });
-};
-
export const clamp = (n, min, max) => Math.max(Math.min(n, max), min);
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index bcd9fa01278..e2fb24f7b57 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -39,12 +39,12 @@ function collapsed(file) {
}
function identifier(file) {
- const { userOrGroup, project, id } = getDerivedMergeRequestInformation({
+ const { namespace, project, id } = getDerivedMergeRequestInformation({
endpoint: file.load_collapsed_diff_url,
});
return uuids({
- seeds: [userOrGroup, project, id, file.file_identifier_hash, file.blob?.id],
+ seeds: [namespace, project, id, file.file_identifier_hash, file.blob?.id],
})[0];
}
diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js
index 6847b8900d2..bc81c0b0a05 100644
--- a/app/assets/javascripts/diffs/utils/merge_request.js
+++ b/app/assets/javascripts/diffs/utils/merge_request.js
@@ -1,6 +1,6 @@
import { ZERO_CHANGES_ALT_DISPLAY } from '../constants';
-const endpointRE = /^(\/?(.+?)\/(.+?)\/-\/merge_requests\/(\d+)).*$/i;
+const endpointRE = /^(\/?(.+\/)+(.+)\/-\/merge_requests\/(\d+)).*$/i;
function getVersionInfo({ endpoint } = {}) {
const dummyRoot = 'https://gitlab.com';
@@ -28,7 +28,7 @@ export function updateChangesTabCount({
export function getDerivedMergeRequestInformation({ endpoint } = {}) {
let mrPath;
- let userOrGroup;
+ let namespace;
let project;
let id;
let diffId;
@@ -36,13 +36,15 @@ export function getDerivedMergeRequestInformation({ endpoint } = {}) {
const matches = endpointRE.exec(endpoint);
if (matches) {
- [, mrPath, userOrGroup, project, id] = matches;
+ [, mrPath, namespace, project, id] = matches;
({ diffId, startSha } = getVersionInfo({ endpoint }));
+
+ namespace = namespace.replace(/\/$/, '');
}
return {
mrPath,
- userOrGroup,
+ namespace,
project,
id,
diffId,
diff --git a/app/assets/javascripts/lib/utils/tappable_promise.js b/app/assets/javascripts/lib/utils/tappable_promise.js
new file mode 100644
index 00000000000..8d327dabe1b
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/tappable_promise.js
@@ -0,0 +1,49 @@
+/**
+ * A promise that is also tappable, i.e. something you can subscribe
+ * to to get progress of a promise until it resolves.
+ *
+ * @example Usage
+ * const tp = new TappablePromise((resolve, reject, tap) => {
+ * for (let i = 0; i < 10; i++) {
+ * tap(i/10);
+ * }
+ * resolve();
+ * });
+ *
+ * tp.tap((progress) => {
+ * console.log(progress);
+ * }).then(() => {
+ * console.log('done');
+ * });
+ *
+ * // Output:
+ * // 0
+ * // 0.1
+ * // 0.2
+ * // ...
+ * // 0.9
+ * // done
+ *
+ *
+ * @param {(resolve: Function, reject: Function, tap: Function) => void} callback
+ * @returns {Promise & { tap: Function }}}
+ */
+export default function TappablePromise(callback) {
+ let progressCallback;
+
+ const promise = new Promise((resolve, reject) => {
+ try {
+ const tap = (progress) => progressCallback?.(progress);
+ resolve(callback(tap, resolve, reject));
+ } catch (e) {
+ reject(e);
+ }
+ });
+
+ promise.tap = function tap(_progressCallback) {
+ progressCallback = _progressCallback;
+ return this;
+ };
+
+ return promise;
+}
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index cfe4baaa1f9..bde7d219e9f 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -67,7 +67,7 @@ export default {
</script>
<template>
<div
- class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-lg gl-border-gray-100 gl-bg-white"
+ class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-lg gl-border-gray-100 gl-bg-white gl-overflow-hidden"
>
<div
v-if="withAlertContainer"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 3c4070105d1..518b28afd9b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -350,8 +350,7 @@ export default {
<template>
<div
ref="gl-form"
- :class="{ 'gl-mt-3 gl-mb-3': addSpacingClasses }"
- class="js-vue-markdown-field md-area position-relative gfm-form"
+ class="js-vue-markdown-field md-area position-relative gfm-form gl-border-none! gl-shadow-none!"
:data-uploads-path="uploadsPath"
>
<markdown-header
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 06d1779b180..af78cc7b5ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -218,7 +218,7 @@ export default {
};
</script>
<template>
- <div>
+ <div class="md-area gl-px-0! gl-overflow-hidden">
<local-storage-sync
:value="editingMode"
as-string
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index f9f4bf260a1..e10a82b5197 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -142,10 +142,13 @@ export default {
return this.isNewDiscussion ? __('Comment') : __('Reply');
},
timelineEntryClass() {
- return this.isNewDiscussion
- ? 'timeline-entry note-form'
- : // eslint-disable-next-line @gitlab/require-i18n-strings
- 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix gl-bg-white! gl-pt-0!';
+ return {
+ 'timeline-entry note-form': this.isNewDiscussion,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix': !this
+ .isNewDiscussion,
+ 'gl-bg-white! gl-pt-0!': this.isEditing,
+ };
},
},
watch: {
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index f9f24366725..cea28b30d42 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -208,7 +208,7 @@ export default {
:form-field-props="formFieldProps"
:add-spacing-classes="false"
data-testid="work-item-add-comment"
- class="gl-mb-3"
+ class="gl-mb-5"
use-bottom-toolbar
supports-quick-actions
:autofocus="autofocus"
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index f3c94732aae..a4cbc430b84 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -69,9 +69,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0];
},
- skip() {
- return !this.workItemIid;
- },
result() {
if (this.isEditing) {
this.checkForConflicts();
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index a0cbf4fcd43..4e3fb819f4c 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -24,11 +24,21 @@
}
}
- img.ProseMirror-selectednode {
- outline: 3px solid rgba($blue-400, 0.48);
+ img.ProseMirror-selectednode,
+ .ProseMirror-selectednode audio,
+ .ProseMirror-selectednode video {
+ outline: 3px solid $blue-200;
outline-offset: -3px;
}
+ video {
+ max-width: 400px;
+ }
+
+ img {
+ max-width: 100%;
+ }
+
ul[data-type='taskList'] {
list-style: none;
padding: 0;
diff --git a/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb
index 648f314a961..82d38ff89d9 100644
--- a/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb
+++ b/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb
@@ -8,11 +8,11 @@ module Resolvers
argument :from, Types::TimeType,
required: true,
- description: 'After the date.'
+ description: 'Timestamp marking the start date and time.'
argument :to, Types::TimeType,
required: true,
- description: 'Before the date.'
+ description: 'Timestamp marking the end date and time.'
def ready?(**args)
start_date = args[:from]
diff --git a/config/metrics/license/20230505112412_installation_creation_date_approximation.yml b/config/metrics/license/20230505112412_installation_creation_date_approximation.yml
new file mode 100644
index 00000000000..5cc7957eca1
--- /dev/null
+++ b/config/metrics/license/20230505112412_installation_creation_date_approximation.yml
@@ -0,0 +1,22 @@
+---
+key_path: installation_creation_date_approximation
+description: "The approximate date and time the instance was installed. Based on the first created user or application setting"
+product_section: analytics
+product_stage: analytics
+product_group: product_intelligence
+value_type: string
+status: active
+milestone: "16.0"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119588
+time_frame: none
+data_source: database
+data_category: operational
+instrumentation_class: InstallationCreationDateApproximationMetric
+performance_indicator_type: []
+distribution:
+ - ce
+ - ee
+tier:
+ - free
+ - premium
+ - ultimate
diff --git a/data/removals/16_0/16-0-graphql-dora-environment-tier-param.yml b/data/removals/16_0/16-0-graphql-dora-environment-tier-param.yml
new file mode 100644
index 00000000000..a4428534cb1
--- /dev/null
+++ b/data/removals/16_0/16-0-graphql-dora-environment-tier-param.yml
@@ -0,0 +1,14 @@
+#
+- title: "Remove the deprecated `environment_tier` parameter from the DORA API"
+ announcement_milestone: "15.2"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: ahegyi
+ stage: Plan
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365939
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The `environment_tier` parameter has been superseded by the `environment_tiers` parameter.
+
+ If you use the `environment_tier` parameter in your integration (REST or GraphQL) then you need to replace it with the `environment_tiers` parameter which accepts an array of strings.
+ tiers: [Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/api/dora/metrics.html
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 9345b4a7d79..b48a9e5dcb3 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -197,6 +197,7 @@ Coursier
CPU
CPUs
CRAN
+CRI-O
cron
crond
cronjob
@@ -275,6 +276,8 @@ desugar
desugars
desynchronized
Dev
+devfile
+devfiles
DevOps
Dhall
dialogs
diff --git a/doc/api/dora/metrics.md b/doc/api/dora/metrics.md
index 9d81a82f520..d30194c3da0 100644
--- a/doc/api/dora/metrics.md
+++ b/doc/api/dora/metrics.md
@@ -28,7 +28,6 @@ GET /projects/:id/dora/metrics
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) can be accessed by the authenticated user. |
| `metric` | string | yes | One of `deployment_frequency`, `lead_time_for_changes`, `time_to_restore_service` or `change_failure_rate`. |
| `end_date` | string | no | Date range to end at. ISO 8601 Date format, for example `2021-03-01`. Default is the current date. |
-| `environment_tier` | string | no | The [tier of the environment](../../ci/environments/index.md#deployment-tier-of-environments). Default is `production`. Deprecated, use `environment_tiers`. |
| `environment_tiers` | array of strings | no | The [tiers of the environments](../../ci/environments/index.md#deployment-tier-of-environments). Default is `production`. |
| `interval` | string | no | The bucketing interval. One of `all`, `monthly` or `daily`. Default is `daily`. |
| `start_date` | string | no | Date range to start from. ISO 8601 Date format, for example `2021-03-01`. Default is 3 months ago. |
@@ -69,7 +68,6 @@ GET /groups/:id/dora/metrics
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) can be accessed by the authenticated user. |
| `metric` | string | yes | One of `deployment_frequency`, `lead_time_for_changes`, `time_to_restore_service` or `change_failure_rate`. |
| `end_date` | string | no | Date range to end at. ISO 8601 Date format, for example `2021-03-01`. Default is the current date. |
-| `environment_tier` | string | no | The [tier of the environment](../../ci/environments/index.md#deployment-tier-of-environments). Default is `production`. Deprecated, use `environment_tiers`. |
| `environment_tiers` | array of strings | no | The [tiers of the environments](../../ci/environments/index.md#deployment-tier-of-environments). Default is `production`. |
| `interval` | string | no | The bucketing interval. One of `all`, `monthly` or `daily`. Default is `daily`. |
| `start_date` | string | no | Date range to start from. ISO 8601 Date format, for example `2021-03-01`. Default is 3 months ago. |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 18e44336223..412556b6657 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3563,6 +3563,44 @@ Input type: `InstanceExternalAuditEventDestinationCreateInput`
| <a id="mutationinstanceexternalauditeventdestinationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationinstanceexternalauditeventdestinationcreateinstanceexternalauditeventdestination"></a>`instanceExternalAuditEventDestination` | [`InstanceExternalAuditEventDestination`](#instanceexternalauditeventdestination) | Destination created. |
+### `Mutation.instanceExternalAuditEventDestinationDestroy`
+
+Input type: `InstanceExternalAuditEventDestinationDestroyInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationinstanceexternalauditeventdestinationdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationinstanceexternalauditeventdestinationdestroyid"></a>`id` | [`AuditEventsInstanceExternalAuditEventDestinationID!`](#auditeventsinstanceexternalauditeventdestinationid) | ID of the external instance audit event destination to destroy. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationinstanceexternalauditeventdestinationdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationinstanceexternalauditeventdestinationdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
+### `Mutation.instanceExternalAuditEventDestinationUpdate`
+
+Input type: `InstanceExternalAuditEventDestinationUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationinstanceexternalauditeventdestinationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationinstanceexternalauditeventdestinationupdatedestinationurl"></a>`destinationUrl` | [`String`](#string) | Destination URL to change. |
+| <a id="mutationinstanceexternalauditeventdestinationupdateid"></a>`id` | [`AuditEventsInstanceExternalAuditEventDestinationID!`](#auditeventsinstanceexternalauditeventdestinationid) | ID of the external instance audit event destination to update. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationinstanceexternalauditeventdestinationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationinstanceexternalauditeventdestinationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationinstanceexternalauditeventdestinationupdateinstanceexternalauditeventdestination"></a>`instanceExternalAuditEventDestination` | [`InstanceExternalAuditEventDestination`](#instanceexternalauditeventdestination) | Updated destination. |
+
### `Mutation.issuableResourceLinkCreate`
Input type: `IssuableResourceLinkCreateInput`
@@ -13801,7 +13839,6 @@ Returns [`[DoraMetric!]`](#dorametric).
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="dorametricsenddate"></a>`endDate` | [`Date`](#date) | Date range to end at. Default is the current date. |
-| <a id="dorametricsenvironmenttier"></a>`environmentTier` **{warning-solid}** | [`DeploymentTier`](#deploymenttier) | **Deprecated** in 15.2. Superseded by `environment_tiers` param. |
| <a id="dorametricsenvironmenttiers"></a>`environmentTiers` | [`[DeploymentTier!]`](#deploymenttier) | Deployment tiers of the environments to return. Defaults to `[PRODUCTION]`. |
| <a id="dorametricsinterval"></a>`interval` | [`DoraMetricBucketingInterval`](#dorametricbucketinginterval) | How the metric should be aggregated. Defaults to `DAILY`. In the case of `ALL`, the `date` field in the response will be `null`. |
| <a id="dorametricsmetric"></a>`metric` **{warning-solid}** | [`DoraMetricType`](#dorametrictype) | **Deprecated** in 15.10. Superseded by metrics fields. See `DoraMetric` type. |
@@ -15789,11 +15826,11 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="groupvaluestreamanalyticsflowmetricscycletimeassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricscycletimeauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="groupvaluestreamanalyticsflowmetricscycletimefrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricscycletimefrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="groupvaluestreamanalyticsflowmetricscycletimelabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricscycletimemilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricscycletimeprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
-| <a id="groupvaluestreamanalyticsflowmetricscycletimeto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="groupvaluestreamanalyticsflowmetricscycletimeto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `GroupValueStreamAnalyticsFlowMetrics.deploymentCount`
@@ -15805,9 +15842,9 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
-| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `GroupValueStreamAnalyticsFlowMetrics.issueCount`
@@ -15821,11 +15858,11 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="groupvaluestreamanalyticsflowmetricsissuecountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuecountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuecountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
-| <a id="groupvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `GroupValueStreamAnalyticsFlowMetrics.issuesCompletedCount`
@@ -15839,11 +15876,11 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
-| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `GroupValueStreamAnalyticsFlowMetrics.leadTime`
@@ -15857,11 +15894,11 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="groupvaluestreamanalyticsflowmetricsleadtimeassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsleadtimeauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="groupvaluestreamanalyticsflowmetricsleadtimefrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsleadtimefrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="groupvaluestreamanalyticsflowmetricsleadtimelabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsleadtimemilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
| <a id="groupvaluestreamanalyticsflowmetricsleadtimeprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. |
-| <a id="groupvaluestreamanalyticsflowmetricsleadtimeto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="groupvaluestreamanalyticsflowmetricsleadtimeto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
### `GroupWikiRepositoryRegistry`
@@ -20447,10 +20484,10 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="projectvaluestreamanalyticsflowmetricscycletimeassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricscycletimeauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricscycletimefrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="projectvaluestreamanalyticsflowmetricscycletimefrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="projectvaluestreamanalyticsflowmetricscycletimelabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricscycletimemilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricscycletimeto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="projectvaluestreamanalyticsflowmetricscycletimeto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `ProjectValueStreamAnalyticsFlowMetrics.deploymentCount`
@@ -20462,8 +20499,8 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
-| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
+| <a id="projectvaluestreamanalyticsflowmetricsdeploymentcountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `ProjectValueStreamAnalyticsFlowMetrics.issueCount`
@@ -20477,10 +20514,10 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="projectvaluestreamanalyticsflowmetricsissuecountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsissuecountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsissuecountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuecountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="projectvaluestreamanalyticsflowmetricsissuecountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsissuecountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `ProjectValueStreamAnalyticsFlowMetrics.issuesCompletedCount`
@@ -20494,10 +20531,10 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
##### `ProjectValueStreamAnalyticsFlowMetrics.leadTime`
@@ -20511,10 +20548,10 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric).
| ---- | ---- | ----------- |
| <a id="projectvaluestreamanalyticsflowmetricsleadtimeassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsleadtimeauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsleadtimefrom"></a>`from` | [`Time!`](#time) | After the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsleadtimefrom"></a>`from` | [`Time!`](#time) | Timestamp marking the start date and time. |
| <a id="projectvaluestreamanalyticsflowmetricsleadtimelabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. |
| <a id="projectvaluestreamanalyticsflowmetricsleadtimemilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. |
-| <a id="projectvaluestreamanalyticsflowmetricsleadtimeto"></a>`to` | [`Time!`](#time) | Before the date. |
+| <a id="projectvaluestreamanalyticsflowmetricsleadtimeto"></a>`to` | [`Time!`](#time) | Timestamp marking the end date and time. |
### `ProjectWikiRepositoryRegistry`
@@ -25701,6 +25738,12 @@ A `AuditEventsExternalAuditEventDestinationID` is a global ID. It is encoded as
An example `AuditEventsExternalAuditEventDestinationID` is: `"gid://gitlab/AuditEvents::ExternalAuditEventDestination/1"`.
+### `AuditEventsInstanceExternalAuditEventDestinationID`
+
+A `AuditEventsInstanceExternalAuditEventDestinationID` is a global ID. It is encoded as a string.
+
+An example `AuditEventsInstanceExternalAuditEventDestinationID` is: `"gid://gitlab/AuditEvents::InstanceExternalAuditEventDestination/1"`.
+
### `AuditEventsStreamingHeaderID`
A `AuditEventsStreamingHeaderID` is a global ID. It is encoded as a string.
diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md
index b9c8e9e1e09..b0c5f3a6a69 100644
--- a/doc/ci/pipelines/index.md
+++ b/doc/ci/pipelines/index.md
@@ -194,6 +194,7 @@ In this example:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. Disabled by default.
> - The `options` keyword was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106038) in GitLab 15.7. Feature flag `run_pipeline_graphql` removed.
+> - The variables list sometimes did not populate correctly due to [a bug](https://gitlab.com/gitlab-org/gitlab/-/issues/386245), which was resolved in GitLab 15.9.
You can define an array of CI/CD variable values the user can select from when running a pipeline manually.
These values are in a dropdown list in the **Run pipeline** page. Add the list of
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index db12f79c821..15b587e3b1e 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -1713,8 +1713,8 @@ object on the mutation. This would allow you to use the
When a user is not allowed to perform the action, or an object is not
found, we should raise a
-`Gitlab::Graphql::Errors::ResourceNotAvailable` error which is
-correctly rendered to the clients.
+`Gitlab::Graphql::Errors::ResourceNotAvailable` by calling `raise_resource_not_available_error!`
+from in the `resolve` method.
### Errors in mutations
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 1d3c5e2ec8f..ac14b1b5ea2 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -39,7 +39,7 @@ The following tools are used:
- [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
this gem makes the translations available in JavaScript. It provides the following Rake task:
- - `rake gettext:po_to_json`: reads the contents of the PO files and generates JSON files that
+ - `rake gettext:compile`: reads the contents of the PO files and generates JS files which
contain all the available translations.
- PO editor: there are multiple applications that can help us work with PO files. A good option is
@@ -1006,4 +1006,4 @@ Suppose you want to add translations for a new language, for example, French:
To manually test Vue translations:
1. Change the GitLab localization to another language than English.
-1. Generate JSON files using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`.
+1. Generate JSON files using `bin/rake gettext:compile`.
diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md
index d75d8a434c1..086d478ac15 100644
--- a/doc/integration/jira/issues.md
+++ b/doc/integration/jira/issues.md
@@ -10,12 +10,14 @@ You can [manage Jira issues directly in GitLab](configure.md).
You can then refer to Jira issues by ID in GitLab commits and merge requests.
The Jira issue IDs must be in uppercase.
-## Reference Jira issues
+## Cross-reference GitLab activity and Jira issues
With this integration, you can cross-reference Jira issues while you work in
-GitLab issues and merge requests. Mention a Jira issue in a GitLab issue,
-merge request, or comment, and GitLab adds a formatted comment to the Jira issue.
-The comment links back to your work in GitLab.
+GitLab issues, merge requests, and Git.
+When you mention a Jira issue in a GitLab issue, merge request, comment, or commit:
+
+- GitLab links to the Jira issue from the mention in GitLab.
+- GitLab adds a formatted comment to the Jira issue that links back to the issue, merge request, or commit in GitLab.
For example, when this commit refers to a `GIT-1` Jira issue:
@@ -23,9 +25,9 @@ For example, when this commit refers to a `GIT-1` Jira issue:
git commit -m "GIT-1 this is a test commit"
```
-GitLab adds to that issue:
+GitLab adds to that Jira issue:
-- A reference in the **Issue links** section
+- A reference in the **Web links** section
- A comment in the **Activity** section that follows this format:
```plaintext
@@ -33,11 +35,15 @@ GitLab adds to that issue:
ENTITY_TITLE
```
-- `USER`: The name of the user who mentioned the issue, linked to their GitLab user profile.
-- `COMMENTLINK`: A link to where the Jira issue was mentioned.
-- `RESOURCE_NAME`: The type of resource, such as a commit or merge request, which referenced the issue.
-- `PROJECT_NAME`: The GitLab project name.
-- `ENTITY_TITLE`: The title of the merge request, or the first line of the commit.
+ - `USER`: Name of the user who has mentioned the Jira issue with a link to their GitLab user profile.
+ - `RESOURCE_NAME`: Type of resource (for example, a GitLab commit, issue, or merge request) that has referenced the Jira issue.
+ - `PROJECT_NAME`: GitLab project name.
+ - `COMMENTLINK`: Link to where the Jira issue is mentioned.
+ - `ENTITY_TITLE`: Title of the GitLab commit (first line), issue, or merge request.
+
+Only a single cross-reference comment appears in Jira per GitLab issue, merge request, or commit.
+For example, multiple comments on a GitLab merge request that reference a Jira issue
+create only a single cross-reference comment back to that merge request in Jira.
You can [disable comments](#disable-comments-on-jira-issues) on issues.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 907d96d8185..cd287d70ca3 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -46,7 +46,7 @@ Linux package, Docker, and self-compiled | Helm chart | Description | Default va
----------------------------|------------|-------------|-----------
`allow_single_sign_on` | `allowSingleSignOn` | List of providers that automatically create a GitLab account. The provider names are available in the **OmniAuth provider name** column in the [supported providers table](#supported-providers). | `false`, which means that signing in using your OmniAuth provider account without a pre-existing GitLab account is not allowed. You must create a GitLab account first, and then connect it to your OmniAuth provider account through your profile settings.
`auto_link_ldap_user` | `autoLinkLdapUser` | Creates an LDAP identity in GitLab for users that are created through an OmniAuth provider. You can enable this setting if you have [LDAP integration](../administration/auth/ldap/index.md) enabled. Requires the `uid` of the user to be the same in both LDAP and the OmniAuth provider. | `false`
-`block_auto_created_users` | `blockAutoCreatedUsers` | Blocks users that are automatically created from signing in until they are approved by an administrator. | `true`. If you set the value to `false`, make sure you define providers that you can control, like SAML or Google. Otherwise, any user on the internet can sign in to GitLab without an administrator's approval.
+`block_auto_created_users` | `blockAutoCreatedUsers` | Places automatically-created users in a [Pending approval](../user/admin_area/moderate_users.md#users-pending-approval) state (unable to sign in) until they are approved by an administrator. | `true`. If you set the value to `false`, make sure you define providers that you can control, like SAML or Google. Otherwise, any user on the internet can sign in to GitLab without an administrator's approval.
### Configure initial settings
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 2085ce94748..64bbaef0f9e 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -136,6 +136,16 @@ In GitLab 13.9, we updated the Omnibus GitLab package and GitLab Helm chart 4.9
GitLab 16.0, we have removed support for Redis 5. If you are using your own Redis 5.0 instance, you must upgrade it to Redis 6.0 or later before upgrading to GitLab 16.0
or later.
+### Remove the deprecated `environment_tier` parameter from the DORA API
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+The `environment_tier` parameter has been superseded by the `environment_tiers` parameter.
+
+If you use the `environment_tier` parameter in your integration (REST or GraphQL) then you need to replace it with the `environment_tiers` parameter which accepts an array of strings.
+
### Removed `external` field from GraphQL `ReleaseAssetLink` type
WARNING:
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index d6aae38df2b..b7d8f6aba58 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -309,6 +309,21 @@ You can follow or unfollow users from either:
In [GitLab 15.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/360755),
the maximum number of users you can follow is 300.
+### Disable following and being followed by other users
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325558) in GitLab 16.0 [with a flag](../feature_flags.md) named `disable_follow_users`. Disabled by default.
+
+You can disable following and being followed by other users.
+
+1. On the top bar, in the upper-right corner, select your avatar.
+1. Select **Edit profile**.
+1. Select **Preferences**.
+1. Clear the **Enable follow users** checkbox.
+1. Select **Save changes**.
+
+NOTE:
+When this feature is being disabled, all current followed/following connections are deleted.
+
## View your activity
GitLab tracks [user contribution activity](contributions_calendar.md).
diff --git a/doc/user/project/remote_development/index.md b/doc/user/project/remote_development/index.md
index fc03bab80b6..66fde8b7b0e 100644
--- a/doc/user/project/remote_development/index.md
+++ b/doc/user/project/remote_development/index.md
@@ -4,7 +4,7 @@ group: IDE
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Remote development **(FREE)**
+# Remote development (Beta) **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95169) in GitLab 15.4 [with a flag](../../../administration/feature_flags.md) named `vscode_web_ide`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/371084) in GitLab 15.7.
@@ -14,15 +14,12 @@ FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use.
WARNING:
-This feature is an [Experiment](../../../policy/alpha-beta-support.md#experiment) and subject to change without notice.
-
-DISCLAIMER:
-This page contains information related to upcoming products, features, and functionality.
-It is important to note that the information presented is for informational purposes only.
-Please do not rely on this information for purchasing or planning purposes.
-As with all projects, the items mentioned on this page are subject to change or delay.
-The development, release, and timing of any products, features, or functionality remain at the
-sole discretion of GitLab Inc.
+This feature is in [Beta](../../../policy/alpha-beta-support.md#beta) and subject to change without notice.
+
+You can use remote development to write and compile code hosted on GitLab. With remote development, you can:
+
+- Create a secure development environment in the cloud.
+- Connect to that environment from your local machine through a web browser or client-based solution.
## Web IDE as a frontend
diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md
index ca404702d72..39b0b9849d7 100644
--- a/doc/user/workspace/index.md
+++ b/doc/user/workspace/index.md
@@ -1,11 +1,195 @@
---
-redirect_to: '../organization/index.md'
-remove_date: '2023-06-13'
+stage: Create
+group: IDE
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-This document was moved to [another location](../organization/index.md).
+# Workspaces (Beta) **(PREMIUM)**
-<!-- This redirect file can be deleted after <2023-06-13>. -->
-<!-- Redirects that point to other docs in the same project expire in three months. -->
-<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
-<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10122) in GitLab 16.0 [with a flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. On GitLab.com, this feature is not available. The feature is not ready for production use.
+
+WARNING:
+This feature is in [Beta](../../policy/alpha-beta-support.md#beta) and subject to change without notice. To leave your feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
+
+You can use workspaces to create and manage isolated development environments for your projects. These environments ensure that different projects don't interfere with each other. Each workspace includes its own set of dependencies, libraries, and tools, which you can customize to meet the specific needs of each project.
+
+With workspaces, you can switch between different development environments and manage your code more effectively.
+
+## Run a workspace
+
+To run a workspace:
+
+1. Set up a Kubernetes cluster that supports the GitLab agent for Kubernetes. See the [supported Kubernetes versions](../clusters/agent/index.md#gitlab-agent-for-kubernetes-supported-cluster-versions).
+1. Ensure autoscaling for Kubernetes cluster is enabled.
+1. In the Kubernetes cluster, verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/) is defined so that volumes can be dynamically provisioned for each workspace.
+1. [Install the GitLab agent for Kubernetes](../clusters/agent/install/index.md).
+1. Configure remote development settings for the GitLab agent with the provided snippet.
+1. Install an Ingress controller of your choice (for example, `ingress-nginx`), and make it accessible over a domain.
+1. [Install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
+1. In each public project you want to use this feature for, define a [devfile](#devfile). Ensure the container images used in the devfile support [arbitrary user IDs](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
+
+## Configure the GitLab agent for Kubernetes
+
+To provision and communicate with the workspace, the GitLab agent for Kubernetes must be running on your cluster. To configure the GitLab agent for Kubernetes:
+
+1. [Install GitLab Runner on the machine where you want to configure the agent](https://docs.gitlab.com/runner/install/).
+1. Deploy the GitLab agent with the provided [YAML manifests](https://gitlab.com/gitlab-examples/ops/gitops-demo/k8s-agents/-/tree/main/manifests). The system does not impose any restrictions on the manner in which pods interact with each other. See [Pod interaction in a cluster](#pod-interaction-in-a-cluster).
+1. Customize the GitLab agent configuration by editing the agent `ConfigMap`. `ConfigMap` is used to configure settings such as the GitLab URL and the registration token. For more information about the available configuration options, see [Connecting a Kubernetes cluster with GitLab](../clusters/agent/index.md).
+1. Deploy the updated `ConfigMap` by running this command:
+
+ ```plaintext
+ kubectl apply -f <path-to-configmap.yaml>
+ ```
+
+1. Configure the agent to run on specific Kubernetes nodes by using labels:
+
+ 1. To assign labels to nodes, use the `kubectl label` command.
+ 1. To configure the agent to only run on nodes with a specific label, use the `nodeSelector` field in the GitLab agent deployment YAML.
+
+You can remove an agent by using the GitLab UI or the GraphQL API. The agent and any associated tokens are removed from GitLab, but no changes are made in your Kubernetes cluster. You must clean up those resources manually. See [Remove an agent](../clusters/agent/work_with_agent.md#remove-an-agent).
+
+## Devfile
+
+A devfile is a file that defines a development environment by specifying the necessary tools, languages, runtimes, and other components for a GitLab project.
+
+Workspaces have built-in support for devfiles. You can specify a devfile for your project in the GitLab configuration file. The devfile is used to automatically configure the development environment with the defined specifications.
+
+This way, you can create consistent and reproducible development environments regardless of the machine or platform you use.
+
+### Relevant schema properties
+
+GitLab only supports the `container` component in [devfile 2.2.0](https://devfile.io/docs/2.2.0/devfile-schema).
+Use this component to define a container image as the execution environment for a devfile workspace.
+You can specify the base image, dependencies, and other settings.
+
+Only these properties are relevant to the GitLab implementation of devfile:
+
+| Properties | Definition |
+|----------------| ----------------------------------------------------------------------------------|
+| `image` | Name of the container image to use for the workspace. |
+| `memoryLimit` | Maximum amount of memory the container can use. |
+| `cpuLimit` | Maximum amount of CPU the container can use. |
+| `mountSources` | Whether to mount the source code directory from the workspace into the container. |
+| `workingDir` | Working directory to use in the container. |
+| `commands` | Commands to run in the container. |
+| `args` | Arguments to pass to the commands. |
+| `ports` | Port mappings to expose from the container. |
+
+### Example definition
+
+The following is an example devfile:
+
+```yaml
+schemaVersion: 2.2.0
+components:
+ - name: tooling-container
+ attributes:
+ gl/inject-editor: true
+ container:
+ image: registry.gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/debian-bullseye-ruby-3.2-node-18.12:rubygems-3.4-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36-gitlab-workspaces
+ endpoints:
+ - name: http-3000
+ targetPort: 3000
+```
+
+For other syntax examples, see the [`demos` projects](https://gitlab.com/gitlab-org/remote-development/demos).
+
+## Create a workspace
+
+Prerequisite:
+
+- You must have [configured the GitLab agent for Kubernetes](#configure-the-gitlab-agent-for-kubernetes).
+
+To create a workspace in GitLab:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. In the root directory of your project, create a file named `.devfile.yaml`.
+1. On the left sidebar, select **Workspaces**.
+1. In the upper right, select **New workspace**.
+1. From the **Select project** dropdown list, select a project with a `.devfile.yaml` file. You can only create workspaces for public projects.
+1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to.
+1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely.
+1. Select **Create Workspace**.
+
+The workspace might take a few minutes to start. When the workspace is ready, use the [Web IDE](../project/web_ide/index.md) to access your development environment.
+You also have access to the terminal and can install any necessary dependencies.
+
+## Web IDE
+
+Workspaces are bundled with the Web IDE by default. The Web IDE is the only code editor available for workspaces.
+
+The Web IDE is powered by the [GitLab VS Code fork](https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork). For more information, see the [Web IDE](../project/web_ide/index.md).
+
+## Private repositories
+
+You cannot create a workspace for a private repository because you cannot verify your identity. You can only clone or access public repositories.
+
+You can clone a public repository over:
+
+- **HTTPS**: You must provide a personal access token every time you access a public repository or create a workspace. This token acts as a password and grants access to a specific resource.
+- **SSH**: You don't have to enter your password or personal access token when you access a public repository. However, you must provide your SSH key or personal access token every time you create a workspace.
+
+## Pod interaction in a cluster
+
+The system does not impose any restrictions on the manner in which pods interact with each other. It's the client's responsibility to restrict network access to the Kubernetes control plane as GitLab cannot determine the location of the API.
+
+Because of this requirement, you might want to isolate this feature from other containers in your cluster.
+
+## Networking and security
+
+Workspaces are isolated environments that are only provisioned when you start a new instance. These environments are isolated from the host machine.
+
+Workspaces use virtual network interfaces to connect to the internet and other resources, which helps prevent conflicts with the host machine's network settings.
+
+### SSL, TLS, and HTTPS
+
+Workspaces use SSL and TLS to provide secure and isolated development environments that you can access from anywhere.
+
+Workspaces support HTTPS, which uses Transport Layer Security (TLS) to encrypt data sent between your machine and the workspace. Workspaces generate and manage their own SSL certificates for HTTPS connections. These SSL certificates are automatically renewed.
+
+Workspaces also support Let's Encrypt SSL certificates, which you can use to enable HTTPS connections with a custom domain name.
+
+### Workspace authorization
+
+To use workspaces, you must have a GitLab account with the necessary permissions to create or access a repository. GitLab authentication is used to control access to workspaces. Only users who have been granted access to a repository can create or access workspaces associated with that repository.
+
+GitLab also provides administrators with the ability to:
+
+- Limit who can create workspaces.
+- Set resource limits for workspaces.
+- Configure the default environment for workspaces.
+
+## Workspace lifecycle
+
+The lifecycle of a workspace is divided into the following stages:
+
+- **Creation**: A workspace is created when you open a new workspace session from a GitLab repository. GitLab creates a virtual machine instance in the cloud with the necessary software and tools for your specific project.
+- **Initialization**: The instance is initialized with the project files and dependencies when you clone the repository or pull from a container registry.
+- **Usage**: The workspace is ready to use. You can use the IDE and command-line tools that come with the workspace or install any other tools.
+- **Persistence**: Any changes made to the project files and dependencies in the workspace persist to the GitLab repository in real-time. This way, these changes can be synced and shared with other collaborators.
+- **Deletion**: When you're finished with the workspace session, you can suspend or delete the workspace. Suspending the workspace pauses billing but keeps the instance running. Deleting the workspace removes the instance and all associated data permanently.
+
+## Container best practices
+
+### Set a user to run a container in Kubernetes
+
+GitLab cannot predict which user is the best fit to run a container in Kubernetes. You must set the user yourself to ensure the container runs correctly.
+
+To set a user to run a container in Kubernetes, follow these best practices:
+
+- When you create a [devfile](#devfile) for the container, ensure the container images used in the devfile support [arbitrary user IDs](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
+- For each container in your project, you must explicitly set the Linux user ID to a random value. The default value for GitLab is `5001`.
+- You must set the fields to prevent any privilege escalation for the Linux user.
+
+CRI-O, the container runtime interface used by OpenShift, has a default group ID of `0` for all containers. If the container images support arbitrary user IDs, all files become editable as a Linux root group member. To solve this issue, GitLab sets arbitrary user IDs for all containers.
+
+### Architectural support
+
+Workspaces use the AMD64 architecture because modern software is generally compatible with this architecture. If you're using other architectures (such as ARM), you can cross-compile your code to run on AMD64 systems.
+
+### Namespace deletion
+
+To delete a namespace, Kubernetes administrators must manually delete the namespace. If you're running a workspace on your own environment, it's your responsibility to manage and delete namespaces.
diff --git a/glfm_specification/output_example_snapshots/html.yml b/glfm_specification/output_example_snapshots/html.yml
index 3cbfc85b7e6..f4a7219b407 100644
--- a/glfm_specification/output_example_snapshots/html.yml
+++ b/glfm_specification/output_example_snapshots/html.yml
@@ -7858,7 +7858,7 @@
static: |-
<p data-sourcepos="1:1-1:33" dir="auto"><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio title"></audio><a href="audio.oga" target="_blank" rel="noopener noreferrer" title="Download 'audio title'">audio title</a></span></p>
wysiwyg: |-
- <p><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio"></audio><a href="audio.oga">audio</a></span></p>
+ <p><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio"></audio><a href="audio.oga" class="with-attachment-icon">audio</a></span></p>
08_01_00__gitlab_internal_extension_markdown__audio__002:
canonical: |
<p><audio src="audio.oga" title="audio title"></audio></p>
@@ -7866,14 +7866,14 @@
<p data-sourcepos="3:1-3:15" dir="auto"><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio title"></audio><a href="audio.oga" target="_blank" rel="noopener noreferrer" title="Download 'audio title'">audio title</a></span></p>
wysiwyg: |-
<pre>[audio]: audio.oga "audio title"</pre>
- <p><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio"></audio><a href="audio.oga">audio</a></span></p>
+ <p><span class="media-container audio-container"><audio src="audio.oga" controls="true" data-setup="{}" data-title="audio"></audio><a href="audio.oga" class="with-attachment-icon">audio</a></span></p>
08_02_00__gitlab_internal_extension_markdown__video__001:
canonical: |
<p><video src="video.m4v" title="video title"></video></p>
static: |-
<p data-sourcepos="1:1-1:33" dir="auto"><span class="media-container video-container"><video src="video.m4v" controls="true" data-setup="{}" data-title="video title" preload="metadata" width="400"></video><a href="video.m4v" target="_blank" rel="noopener noreferrer" title="Download 'video title'">video title</a></span></p>
wysiwyg: |-
- <p><span class="media-container video-container"><video src="video.m4v" controls="true" data-setup="{}" data-title="video"></video><a href="video.m4v">video</a></span></p>
+ <p><span class="media-container video-container"><video src="video.m4v" controls="true" data-setup="{}" data-title="video"></video><a href="video.m4v" class="with-attachment-icon">video</a></span></p>
08_02_00__gitlab_internal_extension_markdown__video__002:
canonical: |
<p><video src="video.mov" title="video title"></video></p>
@@ -7881,7 +7881,7 @@
<p data-sourcepos="3:1-3:15" dir="auto"><span class="media-container video-container"><video src="video.mov" controls="true" data-setup="{}" data-title="video title" preload="metadata" width="400"></video><a href="video.mov" target="_blank" rel="noopener noreferrer" title="Download 'video title'">video title</a></span></p>
wysiwyg: |-
<pre>[video]: video.mov "video title"</pre>
- <p><span class="media-container video-container"><video src="video.mov" controls="true" data-setup="{}" data-title="video"></video><a href="video.mov">video</a></span></p>
+ <p><span class="media-container video-container"><video src="video.mov" controls="true" data-setup="{}" data-title="video"></video><a href="video.mov" class="with-attachment-icon">video</a></span></p>
08_03_00__gitlab_internal_extension_markdown__markdown_preview_api_request_overrides__001:
canonical: |
<p><a href="groups-test-file">groups-test-file</a></p>
@@ -7965,7 +7965,7 @@
static: |-
<p data-sourcepos="1:1-1:46" dir="auto"><span class="media-container audio-container"><audio src="https://gitlab.com/gitlab.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/gitlab.mp3" target="_blank" rel="nofollow noreferrer noopener" title="Download 'Sample Audio'">Sample Audio</a></span></p>
wysiwyg: |-
- <p><span class="media-container audio-container"><audio src="https://gitlab.com/gitlab.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/gitlab.mp3">Sample Audio</a></span></p>
+ <p><span class="media-container audio-container"><audio src="https://gitlab.com/gitlab.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/gitlab.mp3" class="with-attachment-icon">Sample Audio</a></span></p>
08_04_09__gitlab_internal_extension_markdown__migrated_golden_master_examples__audio_and_video_in_lists__001:
canonical: |
TODO: Write canonical HTML for this example
@@ -7990,9 +7990,9 @@
</li>
</ul>
wysiwyg: |-
- <ul bullet="*"><li><p><span class="media-container audio-container"><audio src="https://gitlab.com/1.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/1.mp3">Sample Audio</a></span></p></li><li><p><span class="media-container video-container"><video src="https://gitlab.com/2.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/2.mp4">Sample Video</a></span></p></li></ul>
- <ol parens="false"><li><p><span class="media-container video-container"><video src="https://gitlab.com/1.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/1.mp4">Sample Video</a></span></p></li><li><p><span class="media-container audio-container"><audio src="https://gitlab.com/2.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/2.mp3">Sample Audio</a></span></p></li></ol>
- <ul start="1" parens="false" data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container audio-container"><audio src="https://gitlab.com/1.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/1.mp3">Sample Audio</a></span></p></div></li><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container audio-container"><audio src="https://gitlab.com/2.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/2.mp3">Sample Audio</a></span></p></div></li><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container video-container"><video src="https://gitlab.com/3.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/3.mp4">Sample Video</a></span></p></div></li></ul>
+ <ul bullet="*"><li><p><span class="media-container audio-container"><audio src="https://gitlab.com/1.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/1.mp3" class="with-attachment-icon">Sample Audio</a></span></p></li><li><p><span class="media-container video-container"><video src="https://gitlab.com/2.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/2.mp4" class="with-attachment-icon">Sample Video</a></span></p></li></ul>
+ <ol parens="false"><li><p><span class="media-container video-container"><video src="https://gitlab.com/1.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/1.mp4" class="with-attachment-icon">Sample Video</a></span></p></li><li><p><span class="media-container audio-container"><audio src="https://gitlab.com/2.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/2.mp3" class="with-attachment-icon">Sample Audio</a></span></p></li></ol>
+ <ul start="1" parens="false" data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container audio-container"><audio src="https://gitlab.com/1.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/1.mp3" class="with-attachment-icon">Sample Audio</a></span></p></div></li><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container audio-container"><audio src="https://gitlab.com/2.mp3" controls="true" data-setup="{}" data-title="Sample Audio"></audio><a href="https://gitlab.com/2.mp3" class="with-attachment-icon">Sample Audio</a></span></p></div></li><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p><span class="media-container video-container"><video src="https://gitlab.com/3.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/3.mp4" class="with-attachment-icon">Sample Video</a></span></p></div></li></ul>
08_04_10__gitlab_internal_extension_markdown__migrated_golden_master_examples__blockquote__001:
canonical: |
TODO: Write canonical HTML for this example
@@ -8581,7 +8581,7 @@
static: |-
<p data-sourcepos="1:1-1:46" dir="auto"><span class="media-container video-container"><video src="https://gitlab.com/gitlab.mp4" controls="true" data-setup="{}" data-title="Sample Video" preload="metadata" width="400"></video><a href="https://gitlab.com/gitlab.mp4" target="_blank" rel="nofollow noreferrer noopener" title="Download 'Sample Video'">Sample Video</a></span></p>
wysiwyg: |-
- <p><span class="media-container video-container"><video src="https://gitlab.com/gitlab.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/gitlab.mp4">Sample Video</a></span></p>
+ <p><span class="media-container video-container"><video src="https://gitlab.com/gitlab.mp4" controls="true" data-setup="{}" data-title="Sample Video"></video><a href="https://gitlab.com/gitlab.mp4" class="with-attachment-icon">Sample Video</a></span></p>
08_04_52__gitlab_internal_extension_markdown__migrated_golden_master_examples__word_break__001:
canonical: |
TODO: Write canonical HTML for this example
diff --git a/glfm_specification/output_example_snapshots/prosemirror_json.yml b/glfm_specification/output_example_snapshots/prosemirror_json.yml
index 2ea0c8a6720..8cce959c8f8 100644
--- a/glfm_specification/output_example_snapshots/prosemirror_json.yml
+++ b/glfm_specification/output_example_snapshots/prosemirror_json.yml
@@ -3198,6 +3198,7 @@
"href": "bar",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "bar",
"isReference": false
@@ -3284,6 +3285,7 @@
"href": "foo",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": false
@@ -3805,6 +3807,7 @@
"href": "bar",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "bar",
"isReference": false
@@ -3965,6 +3968,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -4007,6 +4011,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "the title",
"canonicalSrc": "foo",
"isReference": true
@@ -4049,6 +4054,7 @@
"href": "my_(url)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title (with parens)",
"canonicalSrc": "foo*bar\\]",
"isReference": true
@@ -4091,6 +4097,7 @@
"href": "my%20url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo bar",
"isReference": true
@@ -4133,6 +4140,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "\ntitle\nline1\nline2\n",
"canonicalSrc": "foo",
"isReference": true
@@ -4208,6 +4216,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4274,6 +4283,7 @@
"href": "",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4340,6 +4350,7 @@
"href": "/url%5Cbar*baz",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "foo\"bar\\baz",
"canonicalSrc": "foo",
"isReference": true
@@ -4368,6 +4379,7 @@
"href": "url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4410,6 +4422,7 @@
"href": "first",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4480,6 +4493,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4522,6 +4536,7 @@
"href": "/%CF%86%CE%BF%CF%85",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "αγω",
"isReference": true
@@ -4728,6 +4743,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4813,6 +4829,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4859,6 +4876,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -4929,6 +4947,7 @@
"href": "/foo-url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "foo",
"canonicalSrc": "foo",
"isReference": true
@@ -4950,6 +4969,7 @@
"href": "/bar-url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "bar",
"canonicalSrc": "bar",
"isReference": true
@@ -4971,6 +4991,7 @@
"href": "/baz-url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "baz",
"isReference": true
@@ -4999,6 +5020,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -10797,6 +10819,7 @@
"href": "http://example.com?find=%5C*",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com?find=%5C*",
"isReference": false
@@ -10834,6 +10857,7 @@
"href": "/bar*",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "ti*tle",
"canonicalSrc": "/bar*",
"isReference": false
@@ -10862,6 +10886,7 @@
"href": "/bar*",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "ti*tle",
"canonicalSrc": "foo",
"isReference": true
@@ -11023,6 +11048,7 @@
"href": "/f%C3%B6%C3%B6",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "föö",
"canonicalSrc": "/f%C3%B6%C3%B6",
"isReference": false
@@ -11051,6 +11077,7 @@
"href": "/f%C3%B6%C3%B6",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "föö",
"canonicalSrc": "foo",
"isReference": true
@@ -11588,6 +11615,7 @@
"href": "`",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "`",
"isReference": false
@@ -11640,6 +11668,7 @@
"href": "http://foo.bar.%60baz",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar.%60baz",
"isReference": false
@@ -12797,6 +12826,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -13268,6 +13298,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -13338,6 +13369,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -13698,6 +13730,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -14605,6 +14638,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -14637,6 +14671,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url",
"isReference": false
@@ -14791,6 +14826,7 @@
"href": "http://foo.bar/?q=**",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar/?q=**",
"isReference": false
@@ -14823,6 +14859,7 @@
"href": "http://foo.bar/?q=__",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar/?q=__",
"isReference": false
@@ -14899,6 +14936,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "/uri",
"isReference": false
@@ -14927,6 +14965,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -14955,6 +14994,7 @@
"href": "",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "",
"isReference": false
@@ -14983,6 +15023,7 @@
"href": "",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "",
"isReference": false
@@ -15026,6 +15067,7 @@
"href": "/my%20uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/my%20uri",
"isReference": false
@@ -15084,6 +15126,7 @@
"href": "b)c",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "b)c",
"isReference": false
@@ -15151,6 +15194,7 @@
"href": "(foo)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "(foo)",
"isReference": false
@@ -15179,6 +15223,7 @@
"href": "foo(and(bar))",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo(and(bar))",
"isReference": false
@@ -15207,6 +15252,7 @@
"href": "foo(and(bar)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo(and(bar)",
"isReference": false
@@ -15235,6 +15281,7 @@
"href": "foo(and(bar)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo(and(bar)",
"isReference": false
@@ -15263,6 +15310,7 @@
"href": "foo):",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo):",
"isReference": false
@@ -15291,6 +15339,7 @@
"href": "#fragment",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "#fragment",
"isReference": false
@@ -15313,6 +15362,7 @@
"href": "http://example.com#fragment",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com#fragment",
"isReference": false
@@ -15335,6 +15385,7 @@
"href": "http://example.com?foo=3#frag",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com?foo=3#frag",
"isReference": false
@@ -15363,6 +15414,7 @@
"href": "foo%5Cbar",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo%5Cbar",
"isReference": false
@@ -15391,6 +15443,7 @@
"href": "foo%20b%C3%A4",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo%20b%C3%A4",
"isReference": false
@@ -15419,6 +15472,7 @@
"href": "%22title%22",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "%22title%22",
"isReference": false
@@ -15447,6 +15501,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "/url",
"isReference": false
@@ -15475,6 +15530,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title \"\"",
"canonicalSrc": "/url",
"isReference": false
@@ -15503,6 +15559,7 @@
"href": "/url%C2%A0%22title%22",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/url%C2%A0%22title%22",
"isReference": false
@@ -15546,6 +15603,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title \"and\" title",
"canonicalSrc": "/url",
"isReference": false
@@ -15574,6 +15632,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "/uri",
"isReference": false
@@ -15617,6 +15676,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15664,6 +15724,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15692,6 +15753,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15720,6 +15782,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15737,6 +15800,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15757,6 +15821,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15780,6 +15845,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15827,6 +15893,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15858,6 +15925,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15903,6 +15971,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -15975,6 +16044,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -16003,6 +16073,7 @@
"href": "baz*",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "baz*",
"isReference": false
@@ -16098,6 +16169,7 @@
"href": "http://example.com/?search=%5D(uri)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com/?search=%5D(uri)",
"isReference": false
@@ -16126,6 +16198,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "bar",
"isReference": true
@@ -16168,6 +16241,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16210,6 +16284,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16252,6 +16327,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16269,6 +16345,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16289,6 +16366,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16312,6 +16390,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16373,6 +16452,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16418,6 +16498,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uri",
"isReference": false
@@ -16439,6 +16520,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16494,6 +16576,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16518,6 +16601,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16564,6 +16648,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16606,6 +16691,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref",
"isReference": true
@@ -16719,6 +16805,7 @@
"href": "http://example.com/?search=%5D%5Bref%5D",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com/?search=%5D%5Bref%5D",
"isReference": false
@@ -16761,6 +16848,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "bar",
"isReference": true
@@ -16803,6 +16891,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "толпой",
"isReference": true
@@ -16863,6 +16952,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo bar",
"isReference": true
@@ -16895,6 +16985,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "bar",
"isReference": true
@@ -16941,6 +17032,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "bar",
"isReference": true
@@ -17011,6 +17103,7 @@
"href": "/url1",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -17140,6 +17233,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "ref\\[",
"isReference": true
@@ -17196,6 +17290,7 @@
"href": "/uri",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "bar\\\\",
"isReference": true
@@ -17272,6 +17367,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -17314,6 +17410,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17334,6 +17431,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17376,6 +17474,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -17418,6 +17517,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -17464,6 +17564,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -17506,6 +17607,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17526,6 +17628,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17572,6 +17675,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17592,6 +17696,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "*foo* bar",
"isReference": true
@@ -17642,6 +17747,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -17684,6 +17790,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -17726,6 +17833,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -17819,6 +17927,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo*",
"isReference": true
@@ -17847,6 +17956,7 @@
"href": "/url2",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "bar",
"isReference": true
@@ -17903,6 +18013,7 @@
"href": "/url1",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -17945,6 +18056,7 @@
"href": "",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "",
"isReference": false
@@ -17987,6 +18099,7 @@
"href": "/url1",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "foo",
"isReference": true
@@ -18037,6 +18150,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "baz",
"isReference": true
@@ -18079,6 +18193,7 @@
"href": "/url2",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "bar",
"isReference": true
@@ -18096,6 +18211,7 @@
"href": "/url1",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "baz",
"isReference": true
@@ -18156,6 +18272,7 @@
"href": "/url1",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "baz",
"isReference": true
@@ -18901,6 +19018,7 @@
"href": "/url",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": "title",
"canonicalSrc": "foo",
"isReference": true
@@ -18943,6 +19061,7 @@
"href": "http://foo.bar.baz",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar.baz",
"isReference": false
@@ -18971,6 +19090,7 @@
"href": "http://foo.bar.baz/test?q=hello&id=22&boolean",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar.baz/test?q=hello&id=22&boolean",
"isReference": false
@@ -18999,6 +19119,7 @@
"href": null,
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": null,
"isReference": false
@@ -19027,6 +19148,7 @@
"href": "MAILTO:FOO@BAR.BAZ",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "MAILTO:FOO@BAR.BAZ",
"isReference": false
@@ -19055,6 +19177,7 @@
"href": null,
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": null,
"isReference": false
@@ -19083,6 +19206,7 @@
"href": null,
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": null,
"isReference": false
@@ -19111,6 +19235,7 @@
"href": "http://../",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://../",
"isReference": false
@@ -19139,6 +19264,7 @@
"href": null,
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": null,
"isReference": false
@@ -19171,6 +19297,7 @@
"href": "http://foo.bar/baz",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar/baz",
"isReference": false
@@ -19203,6 +19330,7 @@
"href": "http://example.com/%5C%5B%5C",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com/%5C%5B%5C",
"isReference": false
@@ -19231,6 +19359,7 @@
"href": "mailto:foo@bar.example.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:foo@bar.example.com",
"isReference": false
@@ -19259,6 +19388,7 @@
"href": "mailto:foo+special@Bar.baz-bar0.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:foo+special@Bar.baz-bar0.com",
"isReference": false
@@ -19291,6 +19421,7 @@
"href": "mailto:foo+@bar.example.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:foo+@bar.example.com",
"isReference": false
@@ -19342,6 +19473,7 @@
"href": "http://foo.bar",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://foo.bar",
"isReference": false
@@ -19404,6 +19536,7 @@
"href": "http://example.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://example.com",
"isReference": false
@@ -19432,6 +19565,7 @@
"href": "mailto:foo@bar.example.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:foo@bar.example.com",
"isReference": false
@@ -19460,6 +19594,7 @@
"href": "http://www.commonmark.org",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.commonmark.org",
"isReference": false
@@ -19492,6 +19627,7 @@
"href": "http://www.commonmark.org/help",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.commonmark.org/help",
"isReference": false
@@ -19528,6 +19664,7 @@
"href": "http://www.commonmark.org",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.commonmark.org",
"isReference": false
@@ -19558,6 +19695,7 @@
"href": "http://www.commonmark.org/a.b",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.commonmark.org/a.b",
"isReference": false
@@ -19590,6 +19728,7 @@
"href": "http://www.google.com/search?q=Markup+(business)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=Markup+(business)",
"isReference": false
@@ -19612,6 +19751,7 @@
"href": "http://www.google.com/search?q=Markup+(business)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=Markup+(business)",
"isReference": false
@@ -19642,6 +19782,7 @@
"href": "http://www.google.com/search?q=Markup+(business)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=Markup+(business)",
"isReference": false
@@ -19672,6 +19813,7 @@
"href": "http://www.google.com/search?q=Markup+(business)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=Markup+(business)",
"isReference": false
@@ -19700,6 +19842,7 @@
"href": "http://www.google.com/search?q=(business))+ok",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=(business))+ok",
"isReference": false
@@ -19728,6 +19871,7 @@
"href": "http://www.google.com/search?q=commonmark&hl=en",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=commonmark&hl=en",
"isReference": false
@@ -19750,6 +19894,7 @@
"href": "http://www.google.com/search?q=commonmark",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.google.com/search?q=commonmark",
"isReference": false
@@ -19782,6 +19927,7 @@
"href": "http://www.commonmark.org/he",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://www.commonmark.org/he",
"isReference": false
@@ -19814,6 +19960,7 @@
"href": "http://commonmark.org",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "http://commonmark.org",
"isReference": false
@@ -19840,6 +19987,7 @@
"href": "https://encrypted.google.com/search?q=Markup+(business)",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "https://encrypted.google.com/search?q=Markup+(business)",
"isReference": false
@@ -19881,6 +20029,7 @@
"href": "mailto:foo@bar.baz",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:foo@bar.baz",
"isReference": false
@@ -19913,6 +20062,7 @@
"href": "mailto:hello+xyz@mail.example",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:hello+xyz@mail.example",
"isReference": false
@@ -19945,6 +20095,7 @@
"href": "mailto:a.b-c_d@a.b",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:a.b-c_d@a.b",
"isReference": false
@@ -19967,6 +20118,7 @@
"href": "mailto:a.b-c_d@a.b",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "mailto:a.b-c_d@a.b",
"isReference": false
@@ -21198,6 +21350,7 @@
"href": "/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip",
"isReference": false
@@ -21226,6 +21379,7 @@
"href": "/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip",
"isReference": false
@@ -21282,6 +21436,7 @@
"href": "test-file.zip",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "test-file.zip",
"isReference": false
@@ -22966,6 +23121,7 @@
"href": "https://gitlab.com",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "https://gitlab.com",
"isReference": false
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 1190c92ce17..0361adc7fc7 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -60,6 +60,10 @@ module Gitlab
simulate_com? || Gitlab.config.gitlab.url == Gitlab::Saas.com_url || gl_subdomain?
end
+ def self.com_except_jh?
+ com? && !jh?
+ end
+
def self.com
yield if com?
end
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index ceca206b084..669c447c09b 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -24,7 +24,7 @@ module Gitlab
'frame_src' => ContentSecurityPolicy::Directives.frame_src,
'img_src' => "'self' data: blob: http: https:",
'manifest_src' => "'self'",
- 'media_src' => "'self' data: http: https:",
+ 'media_src' => "'self' data: blob: http: https:",
'script_src' => ContentSecurityPolicy::Directives.script_src,
'style_src' => ContentSecurityPolicy::Directives.style_src,
'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index 110c5aa7a19..dc13bb927e6 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -11,13 +11,15 @@ module Gitlab
end
def clear_cache!(key)
+ use_pipeline = ::Feature.enabled?(:use_pipeline_over_multikey)
+
with do |redis|
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
keys << cache_key(key)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.pipelined do |pipeline|
- if ::Feature.enabled?(:use_pipeline_over_multikey)
+ if use_pipeline
keys.each { |key| pipeline.unlink(key) }
else
keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
diff --git a/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric.rb b/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric.rb
new file mode 100644
index 00000000000..e8ae4b4f906
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class InstallationCreationDateApproximationMetric < GenericMetric
+ value do
+ [User.first, ApplicationSetting.first].compact.pluck(:created_at).compact.min
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 5d6d395037c..2522488f579 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -97,9 +97,9 @@ namespace :gitlab do
if Tasks::Gitlab::Assets.head_assets_sha256 != Tasks::Gitlab::Assets.master_assets_sha256
FileUtils.rm_rf([Tasks::Gitlab::Assets::PUBLIC_ASSETS_DIR] + Dir.glob('app/assets/javascripts/locale/**/app.js'))
- # gettext:po_to_json needs to run before rake:assets:precompile because
+ # gettext:compile needs to run before rake:assets:precompile because
# app/assets/javascripts/locale/**/app.js are pre-compiled by Sprockets
- Gitlab::TaskHelpers.invoke_and_time_task('gettext:po_to_json')
+ Gitlab::TaskHelpers.invoke_and_time_task('gettext:compile')
Gitlab::TaskHelpers.invoke_and_time_task('rake:assets:precompile')
log_path = ENV['WEBPACK_COMPILE_LOG_PATH']
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3b426ca7f89..447120aa4e7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18644,6 +18644,9 @@ msgstr ""
msgid "File hooks are similar to system hooks but are executed as files instead of sending data to a URL."
msgstr ""
+msgid "File is too big (%{fileSize}MiB). Max filesize: %{maxFileSize}MiB."
+msgstr ""
+
msgid "File mode changed from %{a_mode} to %{b_mode}"
msgstr ""
@@ -47999,6 +48002,9 @@ msgstr ""
msgid "Uploading..."
msgstr ""
+msgid "Uploading: %{progress}"
+msgstr ""
+
msgid "Upstream"
msgstr ""
diff --git a/qa/gdk/launch b/qa/gdk/launch
index 5d123906f04..249484b417c 100755
--- a/qa/gdk/launch
+++ b/qa/gdk/launch
@@ -36,5 +36,4 @@ cd qa
bundle install --jobs=$(nproc) --retry=3 --quiet
# Run the tests
-bundle exec rake "knapsack:download[test]"
bundle exec bin/qa Test::Instance::All http://gdk.test:3000 -- $RSPEC_ARGS
diff --git a/rubocop/cop/rspec/avoid_conditional_statements.rb b/rubocop/cop/rspec/avoid_conditional_statements.rb
index 48c230a6a7a..d03c3fb5d04 100644
--- a/rubocop/cop/rspec/avoid_conditional_statements.rb
+++ b/rubocop/cop/rspec/avoid_conditional_statements.rb
@@ -21,11 +21,11 @@ module RuboCop
# find('[data-testid="begin-commit-button"]').click
# end
class AvoidConditionalStatements < RuboCop::Cop::Base
- MESSAGE = "Don't use `%{conditional}` conditional statments in specs, it might create flakiness. " \
+ MESSAGE = "Don't use `%{conditional}` conditional statement in specs, it might create flakiness. " \
"See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109"
def on_if(node)
- conditional = node.ternary? ? "#{node.condition.to_s.delete!("\n")} ? (ternary)" : node.keyword
+ conditional = node.ternary? ? node.source : node.keyword
add_offense(node, message: format(MESSAGE, conditional: conditional))
end
diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline
index c0d17443ba9..3f30fb86ccc 100755
--- a/scripts/generate-e2e-pipeline
+++ b/scripts/generate-e2e-pipeline
@@ -5,21 +5,34 @@ set -e
# Script to generate e2e test child pipeline
# This is required because environment variables that are generated dynamically are not picked up by rules in child pipelines
+source "$(dirname "$0")/utils.sh"
source $ENV_FILE
-echo "Generating child pipeline yml definitions for review-app and package-and-test child pipelines"
+echoinfo "Generating child pipeline yml definitions for e2e test pipelines child pipelines"
+
+declare -A qa_pipelines
+
+# key/value pairs for qa pipeline yml definitions
+qa_pipelines["package-and-test-pipeline.yml"]="package-and-test/main.gitlab-ci.yml"
+qa_pipelines["package-and-test-nightly-pipeline.yml"]="package-and-test-nightly/main.gitlab-ci.yml"
+qa_pipelines["review-app-pipeline.yml"]="review-apps/main.gitlab-ci.yml"
+qa_pipelines["test-on-gdk-pipeline.yml"]="test-on-gdk/main.gitlab-ci.yml"
if [ "$QA_SKIP_ALL_TESTS" == "true" ]; then
skip_pipeline=".gitlab/ci/_skip.yml"
- echo "Using ${skip_pipeline} due to QA_SKIP_ALL_TESTS set to 'true'"
- cp $skip_pipeline "$OMNIBUS_PIPELINE_YML"
- cp $skip_pipeline "$REVIEW_PIPELINE_YML"
+ echoinfo "Using ${skip_pipeline} for all e2e test pipelines due to QA_SKIP_ALL_TESTS set to 'true'"
+ for key in "${!qa_pipelines[@]}"; do
+ cp $skip_pipeline "$key"
+ done
+
exit
fi
# set custom cache key to override default cache in pipeline-common because we use bundle to install gitlab-qa gem
qa_cache_key="qa-e2e-ruby-${RUBY_VERSION}-$(md5sum qa/Gemfile.lock | awk '{ print $1 }')"
+# these variables are used across all qa child pipelines
+# it allows to use all features across all child pipelines like skipping all tests, selective test execution etc
variables=$(cat <<YML
variables:
GIT_DEPTH: "20"
@@ -39,12 +52,14 @@ variables:
YML
)
-echo "Using .gitlab/ci/review-apps/main.gitlab-ci.yml and .gitlab/ci/package-and-test/main.gitlab-ci.yml"
-cp .gitlab/ci/review-apps/main.gitlab-ci.yml "$REVIEW_PIPELINE_YML"
-echo "$variables" >>"$REVIEW_PIPELINE_YML"
+echo "***Saving generated qa pipeline files***"
+for key in "${!qa_pipelines[@]}"; do
+ echo "Generating $key"
+
+ cp ".gitlab/ci/${qa_pipelines[$key]}" "$key"
-cp .gitlab/ci/package-and-test/main.gitlab-ci.yml "$OMNIBUS_PIPELINE_YML"
-echo "$variables" >>"$OMNIBUS_PIPELINE_YML"
+ echo >>"$key" # add empty line so it's easier to read if debugging
+ echo "$variables" >>"$key"
+done
-echo "Successfully generated review-app and package-and-test pipeline with following variables section:"
-echo "$variables"
+echoinfo "Successfully generated qa pipeline files"
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index 2a8a1b00692..c79df9c9ed8 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -7,7 +7,7 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import { stubComponent } from 'helpers/stub_component';
import Link from '~/content_editor/extensions/link';
-import { createTestEditor } from '../../test_utils';
+import { createTestEditor, emitEditorEvent, createTransactionWithMeta } from '../../test_utils';
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
@@ -64,7 +64,7 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
tiptapEditor
.chain()
- .insertContent(
+ .setContent(
'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf">PDF File</a>',
)
.setTextSelection(14) // put cursor in the middle of the link
@@ -90,6 +90,36 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect(findLink().text()).toBe('uploads/my_file.pdf');
});
+ it('shows a loading percentage for a file being uploaded', async () => {
+ const setUploadProgress = async (progress) => {
+ const transaction = createTransactionWithMeta('uploadProgress', {
+ filename: 'my_file.pdf',
+ progress,
+ });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor, params: { transaction } });
+ };
+
+ tiptapEditor
+ .chain()
+ .extendMarkRange('link')
+ .updateAttributes('link', { uploading: 'my_file.pdf' })
+ .run();
+
+ await buildWrapperAndDisplayMenu();
+
+ expect(findLink().exists()).toBe(false);
+ expect(wrapper.text()).toContain('Uploading: 0%');
+
+ await setUploadProgress(0.4);
+ expect(wrapper.text()).toContain('Uploading: 40%');
+
+ await setUploadProgress(0.7);
+ expect(wrapper.text()).toContain('Uploading: 70%');
+
+ await setUploadProgress(1);
+ expect(wrapper.text()).toContain('Uploading: 100%');
+ });
+
it('updates the bubble menu state when @selectionUpdate event is triggered', async () => {
const linkUrl = 'https://gitlab.com';
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index e02b36fb8e9..89beb76a6f2 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlLink, GlForm } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
@@ -8,7 +9,12 @@ import Audio from '~/content_editor/extensions/audio';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Video from '~/content_editor/extensions/video';
-import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
+import {
+ createTestEditor,
+ emitEditorEvent,
+ mockChainedCommands,
+ createTransactionWithMeta,
+} from '../../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
@@ -17,7 +23,7 @@ import {
} from '../../test_constants';
const TIPTAP_AUDIO_HTML = `<p>
- <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
const TIPTAP_DIAGRAM_HTML = `<p>
@@ -29,24 +35,23 @@ const TIPTAP_IMAGE_HTML = `<p>
</p>`;
const TIPTAP_VIDEO_HTML = `<p>
- <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+ <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
describe.each`
- mediaType | mediaHTML | filePath | mediaOutputHTML
- ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
- ${'drawio_diagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML}
- ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
- ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'drawioDiagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
`(
'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)',
({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
let wrapper;
let tiptapEditor;
let contentEditor;
- let bubbleMenu;
let eventHub;
const buildEditor = () => {
@@ -68,6 +73,24 @@ describe.each`
});
};
+ const findBubbleMenu = () => wrapper.findComponent(BubbleMenu);
+
+ const showMenu = async () => {
+ findBubbleMenu().vm.$emit('show');
+ await emitEditorEvent({
+ event: 'transaction',
+ tiptapEditor,
+ params: { transaction: createTransactionWithMeta() },
+ });
+ await nextTick();
+ };
+
+ const buildWrapperAndDisplayMenu = () => {
+ buildWrapper();
+
+ return showMenu();
+ };
+
const selectFile = async (file) => {
const input = wrapper.findComponent({ ref: 'fileSelector' });
@@ -83,9 +106,8 @@ describe.each`
expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
};
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
- buildWrapper();
tiptapEditor
.chain()
@@ -94,17 +116,17 @@ describe.each`
.run();
contentEditor.resolveUrl.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`);
+ });
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ it('renders bubble menu component', async () => {
+ await buildWrapperAndDisplayMenu();
- bubbleMenu = wrapper.findComponent(BubbleMenu);
+ expect(findBubbleMenu().classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
- it('renders bubble menu component', () => {
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
- });
+ it('shows a clickable link to the image', async () => {
+ await buildWrapperAndDisplayMenu();
- it('shows a clickable link to the image', () => {
const link = wrapper.findComponent(GlLink);
expect(link.attributes()).toEqual(
expect.objectContaining({
@@ -117,8 +139,41 @@ describe.each`
expect(link.text()).toBe(filePath);
});
+ it('shows a loading percentage for a file being uploaded', async () => {
+ jest.spyOn(tiptapEditor, 'isActive').mockImplementation((name) => name === mediaType);
+
+ await buildWrapperAndDisplayMenu();
+
+ const setUploadProgress = async (progress) => {
+ const transaction = createTransactionWithMeta('uploadProgress', {
+ filename: filePath,
+ progress,
+ });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor, params: { transaction } });
+ };
+
+ tiptapEditor.chain().selectAll().updateAttributes(mediaType, { uploading: filePath }).run();
+
+ await emitEditorEvent({ event: 'selectionUpdate', tiptapEditor });
+
+ expect(wrapper.findComponent(GlLink).exists()).toBe(false);
+ expect(wrapper.text()).toContain('Uploading: 0%');
+
+ await setUploadProgress(0.4);
+
+ expect(wrapper.text()).toContain('Uploading: 40%');
+
+ await setUploadProgress(0.7);
+ expect(wrapper.text()).toContain('Uploading: 70%');
+
+ await setUploadProgress(1);
+ expect(wrapper.text()).toContain('Uploading: 100%');
+ });
+
describe('when BubbleMenu emits hidden event', () => {
it('resets media bubble menu state', async () => {
+ await buildWrapperAndDisplayMenu();
+
// Switch to edit mode to access component state in form fields
await wrapper.findByTestId('edit-media').vm.$emit('click');
@@ -137,6 +192,8 @@ describe.each`
describe('copy button', () => {
it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
+ await buildWrapperAndDisplayMenu();
+
jest.spyOn(navigator.clipboard, 'writeText');
await wrapper.findByTestId('copy-media-src').vm.$emit('click');
@@ -147,6 +204,8 @@ describe.each`
describe(`remove ${mediaType} button`, () => {
it(`removes the ${mediaType}`, async () => {
+ await buildWrapperAndDisplayMenu();
+
await wrapper.findByTestId('delete-media').vm.$emit('click');
expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
@@ -154,7 +213,9 @@ describe.each`
});
describe(`replace ${mediaType} button`, () => {
- if (mediaType !== 'drawio_diagram') {
+ beforeEach(buildWrapperAndDisplayMenu);
+
+ if (mediaType !== 'drawioDiagram') {
it('uploads and replaces the selected image when file input changes', async () => {
const commands = mockChainedCommands(tiptapEditor, [
'focus',
@@ -195,6 +256,8 @@ describe.each`
let mediaAltInput;
beforeEach(async () => {
+ await buildWrapperAndDisplayMenu();
+
await wrapper.findByTestId('edit-media').vm.$emit('click');
mediaSrcInput = wrapper.findByTestId('media-src');
diff --git a/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
index 06ea863dbfa..c6793d5b01b 100644
--- a/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
@@ -16,11 +16,11 @@ describe('content_editor/components/toolbar_attachment_button', () => {
});
};
- const selectFile = async (file) => {
+ const selectFiles = async (...files) => {
const input = wrapper.findComponent({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
- Object.defineProperty(input.element, 'files', { value: [file], writable: true });
+ Object.defineProperty(input.element, 'files', { value: files, writable: true });
await input.trigger('change');
};
@@ -28,6 +28,7 @@ describe('content_editor/components/toolbar_attachment_button', () => {
editor = createTestEditor({
extensions: [
Link,
+ Image,
Attachment.configure({
renderMarkdown: jest.fn(),
uploadsPath: '/uploads/',
@@ -44,12 +45,14 @@ describe('content_editor/components/toolbar_attachment_button', () => {
it('uploads the selected attachment when file input changes', async () => {
const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+ const file1 = new File(['foo'], 'foo.png', { type: 'image/png' });
+ const file2 = new File(['bar'], 'bar.png', { type: 'image/png' });
- await selectFile(file);
+ await selectFiles(file1, file2);
expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file: file1 });
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file: file2 });
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link', value: 'upload' }]);
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index 3c699b05b0f..f037ac520fe 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -1,17 +1,15 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
-import Loading from '~/content_editor/extensions/loading';
import { VARIANT_DANGER } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, expectDocumentAfterTransaction } from '../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML,
@@ -29,7 +27,6 @@ describe('content_editor/extensions/attachment', () => {
let audio;
let drawioDiagram;
let video;
- let loading;
let link;
let renderMarkdown;
let mock;
@@ -40,35 +37,35 @@ describe('content_editor/extensions/attachment', () => {
const imageFileSvg = new File(['foo'], 'test-file.svg', { type: 'image/svg+xml' });
const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' });
const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' });
+ const videoFile1 = new File(['foo'], 'test-file1.mp4', { type: 'video/mp4' });
const drawioDiagramFile = new File(['foo'], 'test-file.drawio.svg', { type: 'image/svg+xml' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
-
- const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
- return new Promise((resolve) => {
- let counter = 1;
- const handleTransaction = async () => {
- if (counter === number) {
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
- tiptapEditor.off('update', handleTransaction);
- await waitForPromises();
- resolve();
- }
-
- counter += 1;
- };
-
- tiptapEditor.on('update', handleTransaction);
- action();
- });
+ const attachmentFile1 = new File(['foo'], 'test-file1.zip', { type: 'application/zip' });
+ const attachmentFile2 = new File(['foo'], 'test-file2.zip', { type: 'application/zip' });
+
+ const markdownApiResult = {
+ 'test-file.png': PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ 'test-file.svg': PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML,
+ 'test-file.mp3': PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ 'test-file.mp4': PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ 'test-file1.mp4': PROJECT_WIKI_ATTACHMENT_VIDEO_HTML.replace(/test-file/g, 'test-file1'),
+ 'test-file.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+ 'test-file1.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML.replace(/test-file/g, 'test-file1'),
+ 'test-file2.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML.replace(/test-file/g, 'test-file2'),
+ 'test-file.drawio.svg': PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
};
+ const [, group, project] = markdownApiResult[attachmentFile.name].match(
+ /\/(group[0-9]+)\/(project[0-9]+)\//,
+ );
+ const blobUrl = 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b';
+
beforeEach(() => {
renderMarkdown = jest.fn();
eventHub = eventHubFactory();
tiptapEditor = createTestEditor({
extensions: [
- Loading,
Link,
Image,
Audio,
@@ -79,11 +76,10 @@ describe('content_editor/extensions/attachment', () => {
});
({
- builders: { doc, p, image, audio, video, loading, link, drawioDiagram },
+ builders: { doc, p, image, audio, video, link, drawioDiagram },
} = createDocBuilder({
tiptapEditor,
names: {
- loading: { markType: Loading.name },
image: { nodeType: Image.name },
link: { nodeType: Link.name },
audio: { nodeType: Audio.name },
@@ -105,6 +101,14 @@ describe('content_editor/extensions/attachment', () => {
${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined}
${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
`('handles $eventType properly', ({ eventType, propName, eventData, output }) => {
+ mock.onPost().reply(HTTP_STATUS_OK, {
+ link: {
+ markdown: `![test-file](test-file.png)`,
+ },
+ });
+
+ renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
+
const event = Object.assign(new Event(eventType), eventData);
const handled = tiptapEditor.view.someProp(propName, (eventHandler) => {
return eventHandler(tiptapEditor.view, event);
@@ -121,15 +125,13 @@ describe('content_editor/extensions/attachment', () => {
});
describe.each`
- nodeType | mimeType | html | file | mediaType
- ${'image (png)'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
- ${'image (svg)'} | ${'image/svg+xml'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML} | ${imageFileSvg} | ${(attrs) => image(attrs)}
- ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
- ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
- ${'drawioDiagram'} | ${'image/svg+xml'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)}
- `('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => {
- const base64EncodedFile = `data:${mimeType};base64,Zm9v`;
-
+ nodeType | html | file | mediaType
+ ${'image (png)'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
+ ${'image (svg)'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML} | ${imageFileSvg} | ${(attrs) => image(attrs)}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
+ ${'drawioDiagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)}
+ `('when the file is $nodeType', ({ html, file, mediaType }) => {
beforeEach(() => {
renderMarkdown.mockResolvedValue(html);
});
@@ -145,10 +147,13 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
- it('inserts a media content with src set to the encoded content and uploading true', async () => {
- const expectedDoc = doc(p(mediaType({ uploading: true, src: base64EncodedFile })));
+ it('inserts a media content with src set to the encoded content and uploading=file_name', async () => {
+ const expectedDoc = doc(
+ p(mediaType({ uploading: file.name, src: blobUrl, alt: file.name })),
+ );
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 1,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -160,7 +165,7 @@ describe('content_editor/extensions/attachment', () => {
p(
mediaType({
canonicalSrc: file.name,
- src: base64EncodedFile,
+ src: blobUrl,
alt: expect.stringContaining('test-file'),
uploading: false,
}),
@@ -168,6 +173,7 @@ describe('content_editor/extensions/attachment', () => {
);
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -175,6 +181,25 @@ describe('content_editor/extensions/attachment', () => {
});
});
+ describe('when uploading a large file', () => {
+ beforeEach(() => {
+ // Set max file size to 1 byte, our file is 3 bytes
+ gon.max_file_size = 1 / 1024 / 1024;
+ });
+
+ it('emits an alert event that includes an error message', () => {
+ tiptapEditor.commands.uploadAttachment({ file });
+
+ return new Promise((resolve) => {
+ eventHub.$on('alert', ({ message, variant }) => {
+ expect(variant).toBe(VARIANT_DANGER);
+ expect(message).toContain('File is too big');
+ resolve();
+ });
+ });
+ });
+ });
+
describe('when uploading request fails', () => {
beforeEach(() => {
mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
@@ -184,6 +209,7 @@ describe('content_editor/extensions/attachment', () => {
const expectedDoc = doc(p(''));
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -205,10 +231,8 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
- const markdownApiResult = PROJECT_WIKI_ATTACHMENT_LINK_HTML;
-
beforeEach(() => {
- renderMarkdown.mockResolvedValue(markdownApiResult);
+ renderMarkdown.mockResolvedValue(markdownApiResult[attachmentFile.name]);
});
describe('when uploading succeeds', () => {
@@ -222,18 +246,20 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
- it('inserts a loading mark', async () => {
- const expectedDoc = doc(p(loading({ label: 'test-file' })));
+ it('inserts a link with a blob url', async () => {
+ const expectedDoc = doc(
+ p(link({ uploading: attachmentFile.name, href: blobUrl }, 'test-file.zip')),
+ );
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 1,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
});
});
- it('updates the loading mark with a link with canonicalSrc and href attrs', async () => {
- const [, group, project] = markdownApiResult.match(/\/(group[0-9]+)\/(project[0-9]+)\//);
+ it('updates the blob url link with an actual link with canonicalSrc and href attrs', async () => {
const expectedDoc = doc(
p(
link(
@@ -241,12 +267,13 @@ describe('content_editor/extensions/attachment', () => {
canonicalSrc: 'test-file.zip',
href: `/${group}/${project}/-/wikis/test-file.zip`,
},
- 'test-file',
+ 'test-file.zip',
),
),
);
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
@@ -263,6 +290,7 @@ describe('content_editor/extensions/attachment', () => {
const expectedDoc = doc(p(''));
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
@@ -279,5 +307,433 @@ describe('content_editor/extensions/attachment', () => {
});
});
});
+
+ describe('uploading multiple files', () => {
+ const uploadMultipleFiles = () => {
+ const files = [
+ attachmentFile,
+ imageFile,
+ videoFile,
+ attachmentFile1,
+ attachmentFile2,
+ videoFile1,
+ audioFile,
+ ];
+
+ for (const file of files) {
+ renderMarkdown.mockImplementation((markdown) =>
+ Promise.resolve(markdownApiResult[markdown.match(/\((.+?)\)$/)[1]]),
+ );
+
+ mock
+ .onPost()
+ .replyOnce(HTTP_STATUS_OK, { link: { markdown: `![test-file](${file.name})` } });
+
+ tiptapEditor.commands.uploadAttachment({ file });
+ }
+ };
+
+ it.each([
+ [1, () => doc(p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')))],
+ [
+ 2,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ ),
+ ],
+ [
+ 3,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ ),
+ ],
+ [
+ 4,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ ),
+ ],
+ [
+ 5,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ ),
+ ],
+ [
+ 6,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ ),
+ ],
+ [
+ 7,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 8,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 9,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 10,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 11,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 12,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 13,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(
+ video({
+ alt: 'test-file1.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file1.mp4',
+ uploading: false,
+ }),
+ ),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 14,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(
+ video({
+ alt: 'test-file1.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file1.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ audio({
+ alt: 'test-file.mp3',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp3',
+ uploading: false,
+ }),
+ ),
+ ),
+ ],
+ ])('uploads all files of mixed types successfully (tx %i)', async (n, document) => {
+ await expectDocumentAfterTransaction({
+ tiptapEditor,
+ number: n,
+ expectedDoc: document(),
+ action: uploadMultipleFiles,
+ });
+ });
+
+ it('cleans up the state if all uploads fail', async () => {
+ await expectDocumentAfterTransaction({
+ tiptapEditor,
+ number: 14,
+ expectedDoc: doc(p(), p(), p(), p(), p(), p(), p()),
+ action: () => {
+ // Set max file size to 1 byte, our file is 3 bytes
+ gon.max_file_size = 1 / 1024 / 1024;
+ uploadMultipleFiles();
+ },
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index ead898554d1..3c6f28f0c32 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -31,11 +31,6 @@ describe('content_editor/extensions/link', () => {
${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))}
${'text'} | ${() => p('text')}
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
- ${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
- ${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
- ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
- ${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
- ${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 506db4144fc..3729b303cc6 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -281,17 +281,18 @@ hi
).toBe('![GitLab][gitlab-url]');
});
- it('omits image data urls when serializing', () => {
+ it.each`
+ src
+ ${'data:image/png;base64,iVBORw0KGgoAAAAN'}
+ ${'blob:https://gitlab.com/1234-5678-9012-3456'}
+ `('omits images with data/blob urls when serializing', ({ src }) => {
+ expect(serialize(paragraph(image({ src, alt: 'image' })))).toBe('');
+ });
+
+ it('does not escape url in an image', () => {
expect(
- serialize(
- paragraph(
- image({
- src: 'data:image/png;base64,iVBORw0KGgoAAAAN',
- alt: 'image',
- }),
- ),
- ),
- ).toBe('![image]()');
+ serialize(paragraph(image({ src: 'https://example.com/image__1_.png', alt: 'image' }))),
+ ).toBe('![image](https://example.com/image__1_.png)');
});
it('correctly serializes strikethrough', () => {
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 16f90a15c24..1f4a367e46c 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -5,6 +5,7 @@ import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
@@ -63,6 +64,12 @@ export const emitEditorEvent = ({ tiptapEditor, event, params = {} }) => {
return nextTick();
};
+export const createTransactionWithMeta = (metaKey, metaValue) => {
+ return {
+ getMeta: (key) => (key === metaKey ? metaValue : null),
+ };
+};
+
/**
* Creates an instance of the Tiptap Editor class
* with a minimal configuration for testing purposes.
@@ -205,6 +212,24 @@ export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} })
});
};
+export const expectDocumentAfterTransaction = ({ tiptapEditor, number, expectedDoc, action }) => {
+ return new Promise((resolve) => {
+ let counter = 0;
+ const handleTransaction = async () => {
+ counter += 1;
+ if (counter === number) {
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ tiptapEditor.off('update', handleTransaction);
+ await waitForPromises();
+ resolve();
+ }
+ };
+
+ tiptapEditor.on('update', handleTransaction);
+ action();
+ });
+};
+
export const createTiptapEditor = (extensions = []) =>
createTestEditor({
extensions: [
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index 21599a3be45..11c0efb9a9c 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -8,7 +8,7 @@ import { diffMetadata } from '../mock_data/diff_metadata';
describe('Merge Request utilities', () => {
const derivedBaseInfo = {
mrPath: '/gitlab-org/gitlab-test/-/merge_requests/4',
- userOrGroup: 'gitlab-org',
+ namespace: 'gitlab-org',
project: 'gitlab-test',
id: '4',
};
@@ -22,7 +22,7 @@ describe('Merge Request utilities', () => {
};
const unparseableEndpoint = {
mrPath: undefined,
- userOrGroup: undefined,
+ namespace: undefined,
project: undefined,
id: undefined,
...noVersion,
@@ -79,29 +79,41 @@ describe('Merge Request utilities', () => {
});
describe('getDerivedMergeRequestInformation', () => {
- let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
+ const bare = diffMetadata.latest_version_path;
it.each`
- argument | response
- ${{ endpoint }} | ${{ ...derivedBaseInfo, ...noVersion }}
- ${{}} | ${unparseableEndpoint}
- ${{ endpoint: undefined }} | ${unparseableEndpoint}
- ${{ endpoint: null }} | ${unparseableEndpoint}
+ argument | response
+ ${{ endpoint: `${bare}.json?searchParam=irrelevant` }} | ${{ ...derivedBaseInfo, ...noVersion }}
+ ${{}} | ${unparseableEndpoint}
+ ${{ endpoint: undefined }} | ${unparseableEndpoint}
+ ${{ endpoint: null }} | ${unparseableEndpoint}
`('generates the correct derived results based on $argument', ({ argument, response }) => {
expect(getDerivedMergeRequestInformation(argument)).toStrictEqual(response);
});
- describe('version information', () => {
- const bare = diffMetadata.latest_version_path;
- endpoint = diffMetadata.merge_request_diffs[0].compare_path;
+ describe('sub-group namespace', () => {
+ it('extracts the entire namespace plus the project name', () => {
+ const { namespace, project } = getDerivedMergeRequestInformation({
+ endpoint: `/some/deep/path/of/groups${bare}`,
+ });
+ expect(namespace).toBe('some/deep/path/of/groups/gitlab-org');
+ expect(project).toBe('gitlab-test');
+ });
+ });
+
+ describe('version information', () => {
it('still gets the correct derived information', () => {
- expect(getDerivedMergeRequestInformation({ endpoint })).toMatchObject(derivedBaseInfo);
+ expect(
+ getDerivedMergeRequestInformation({
+ endpoint: diffMetadata.merge_request_diffs[0].compare_path,
+ }),
+ ).toMatchObject(derivedBaseInfo);
});
it.each`
url | versionPart
- ${endpoint} | ${derivedVersionInfo}
+ ${diffMetadata.merge_request_diffs[0].compare_path} | ${derivedVersionInfo}
${`${bare}?diff_id=${derivedVersionInfo.diffId}`} | ${{ ...derivedVersionInfo, startSha: undefined }}
${`${bare}?start_sha=${derivedVersionInfo.startSha}`} | ${{ ...derivedVersionInfo, diffId: undefined }}
`(
diff --git a/spec/frontend/lib/utils/tappable_promise_spec.js b/spec/frontend/lib/utils/tappable_promise_spec.js
new file mode 100644
index 00000000000..654cd20a9de
--- /dev/null
+++ b/spec/frontend/lib/utils/tappable_promise_spec.js
@@ -0,0 +1,63 @@
+import TappablePromise from '~/lib/utils/tappable_promise';
+
+describe('TappablePromise', () => {
+ it('allows a promise to have a progress indicator', () => {
+ const pseudoProgress = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
+ const progressCallback = jest.fn();
+ const promise = new TappablePromise((tap, resolve) => {
+ pseudoProgress.forEach(tap);
+ resolve('done');
+
+ return 'returned value';
+ });
+
+ return promise
+ .tap(progressCallback)
+ .then((val) => {
+ expect(val).toBe('done');
+ expect(val).not.toBe('returned value');
+
+ expect(progressCallback).toHaveBeenCalledTimes(pseudoProgress.length);
+
+ pseudoProgress.forEach((progress, index) => {
+ expect(progressCallback).toHaveBeenNthCalledWith(index + 1, progress);
+ });
+ })
+ .catch(() => {});
+ });
+
+ it('resolves with the value returned by the callback', () => {
+ const promise = new TappablePromise((tap) => {
+ tap(0.5);
+ return 'test';
+ });
+
+ return promise
+ .tap((progress) => {
+ expect(progress).toBe(0.5);
+ })
+ .then((value) => {
+ expect(value).toBe('test');
+ });
+ });
+
+ it('allows a promise to be rejected', () => {
+ const promise = new TappablePromise((tap, resolve, reject) => {
+ reject(new Error('test error'));
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+
+ it('rejects the promise if the callback throws an error', () => {
+ const promise = new TappablePromise(() => {
+ throw new Error('test error');
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index ab259249772..e54b87c307c 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -9,12 +9,7 @@ import RefreshButton from '~/monitoring/components/refresh_button.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import {
- environmentData,
- dashboardGitResponse,
- selfMonitoringDashboardGitResponse,
- dashboardHeaderProps,
-} from '../mock_data';
+import { environmentData, dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
const mockProjectPath = 'https://path/to/project';
@@ -267,14 +262,8 @@ describe('Dashboard header', () => {
});
describe('actions menu', () => {
- const ootbDashboards = [
- dashboardGitResponse[0].path,
- selfMonitoringDashboardGitResponse[0].path,
- ];
- const customDashboards = [
- dashboardGitResponse[1].path,
- selfMonitoringDashboardGitResponse[1].path,
- ];
+ const ootbDashboards = [dashboardGitResponse[0].path];
+ const customDashboards = [dashboardGitResponse[1].path];
it('is rendered', () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 00be5868ba3..1d23190e586 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -206,32 +206,6 @@ export const dashboardGitResponse = [
...customDashboardsData,
];
-export const selfMonitoringDashboardGitResponse = [
- {
- default: true,
- display_name: 'Default',
- can_edit: false,
- system_dashboard: true,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/self_monitoring_default.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/self_monitoring_default.yml`,
- },
- {
- default: false,
- display_name: 'dashboard.yml',
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/-/blob/main/.gitlab/dashboards/dashboard.yml`,
- path: '.gitlab/dashboards/dashboard.yml',
- starred: true,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
- },
- ...customDashboardsData,
-];
-
// Metrics mocks
export const metricsResult = [
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index c8d972b19a3..17681c08dbd 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -24,7 +24,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
</div>
<div
- class="js-vue-markdown-field md-area position-relative gfm-form js-expanded"
+ class="js-vue-markdown-field md-area position-relative gfm-form gl-border-none! gl-shadow-none! js-expanded"
data-uploads-path=""
>
<markdown-header-stub
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index b7877784a2d..62cbb1bacb6 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -303,12 +303,6 @@ describe('WorkItemDescription', () => {
expect(workItemResponseHandler).toHaveBeenCalled();
});
-
- it('skips calling the work item query when missing workItemIid', async () => {
- await createComponent({ workItemIid: null });
-
- expect(workItemResponseHandler).not.toHaveBeenCalled();
- });
},
);
});
diff --git a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
index d9dd1b387dc..81b00f82803 100644
--- a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
@@ -74,9 +74,11 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
it 'adds a PostgresAsyncConstraintValidation record' do
expect { prepare }.to change {
Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.count
- }.from(0).to(1)
+ }.by(1)
+
+ record = Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation
+ .where(table_name: table_name).last
- record = Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.last
expect(record.name).to eq described_class::PARTITIONING_CONSTRAINT_NAME
expect(record).to be_check_constraint
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 4a0a740f121..571c67db597 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -547,10 +547,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it 'deletes those pertaining to the given table' do
expect { migration.cleanup_partitioning_data_migration(source_table) }
- .to change { ::Gitlab::Database::BackgroundMigration::BatchedMigration.count }.from(2).to(1)
+ .to change { ::Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(-1)
- remaining_record = ::Gitlab::Database::BackgroundMigration::BatchedMigration.first
- expect(remaining_record.table_name).to eq('other_table')
+ expect(::Gitlab::Database::BackgroundMigration::BatchedMigration.where(table_name: 'other_table').any?)
+ .to be_truthy
+
+ expect(::Gitlab::Database::BackgroundMigration::BatchedMigration.where(table_name: source_table).any?)
+ .to be_falsy
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric_spec.rb
new file mode 100644
index 00000000000..11e1139e542
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_approximation_metric_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InstallationCreationDateApproximationMetric,
+ feature_category: :service_ping do
+ let_it_be(:application_setting) { create(:application_setting) }
+
+ context 'with a root user' do
+ let_it_be(:root) { create(:user, id: 1, created_at: DateTime.current - 2.days) }
+ let_it_be(:expected_value) { root.reload.created_at } # reloading to get the timestamp from the database
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+
+ context 'without a root user' do
+ let_it_be(:another_user) { create(:user, id: 2, created_at: DateTime.current + 2.days) }
+ let_it_be(:expected_value) { application_setting.reload.created_at }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c44bb64a5c0..8ed0a2df586 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -132,6 +132,28 @@ RSpec.describe Gitlab do
end
end
+ describe '.com_except_jh?' do
+ subject { described_class.com_except_jh? }
+
+ before do
+ allow(described_class).to receive(:com?).and_return(com?)
+ allow(described_class).to receive(:jh?).and_return(jh?)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:com?, :jh?, :expected) do
+ true | true | false
+ true | false | true
+ false | true | false
+ false | false | false
+ end
+
+ with_them do
+ it { is_expected.to eq(expected) }
+ end
+ end
+
describe '.com' do
subject { described_class.com { true } }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index a30f4474d3c..d8019e74c71 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- shared_examples 'transitioning to pre_importing', skip_pre_import_success: true do
+ shared_examples 'transitioning to pre_importing' do
before do
repository.update_column(:migration_pre_import_done_at, Time.zone.now)
end
@@ -145,7 +145,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
end
- shared_examples 'transitioning to importing', skip_import_success: true do
+ shared_examples 'transitioning to importing' do
before do
repository.update_columns(migration_import_done_at: Time.zone.now)
end
@@ -219,9 +219,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.start_pre_import }
before do |example|
- unless example.metadata[:skip_pre_import_success]
- allow(repository).to receive(:migration_pre_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[default pre_importing importing import_aborted]
@@ -234,9 +232,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.retry_pre_import }
before do |example|
- unless example.metadata[:skip_pre_import_success]
- allow(repository).to receive(:migration_pre_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_pre_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
@@ -264,9 +260,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.start_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_import_done pre_importing importing import_aborted]
@@ -279,9 +273,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.retry_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
@@ -374,9 +366,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
subject { repository.finish_pre_import_and_start_import }
before do |example|
- unless example.metadata[:skip_import_success]
- allow(repository).to receive(:migration_import).and_return(:ok)
- end
+ allow(repository).to receive(:migration_import).and_return(:ok)
end
it_behaves_like 'transitioning from allowed states', %w[pre_importing importing import_aborted]
diff --git a/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb
index d2f5e4aa619..3f45f660aa5 100644
--- a/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb
+++ b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe RuboCop::Cop::RSpec::AvoidConditionalStatements, feature_category
it 'flags if conditional' do
expect_offense(<<~RUBY)
if page.has_css?('[data-testid="begin-commit-button"]')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `if` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `if` conditional statement in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
find('[data-testid="begin-commit-button"]').click
end
RUBY
@@ -20,7 +20,7 @@ RSpec.describe RuboCop::Cop::RSpec::AvoidConditionalStatements, feature_category
RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do
it 'creates directory in current directory' do
unless page.has_css?('[data-testid="begin-commit-button"]')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `unless` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `unless` conditional statement in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
find('[data-testid="begin-commit-button"]').click
end
end
@@ -33,7 +33,7 @@ RSpec.describe RuboCop::Cop::RSpec::AvoidConditionalStatements, feature_category
RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do
it 'creates directory in current directory' do
user.present ? user : nil
- ^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `(send (send nil :user) :present) ? (ternary)` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `user.present ? user : nil` conditional statement in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109
end
end
RUBY
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index bfc25877f98..f2194f46ab4 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -818,6 +818,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
"href": "/uploads/groups-test-file",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "/uploads/groups-test-file",
"isReference": false
@@ -846,6 +847,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
"href": "projects-test-file",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "projects-test-file",
"isReference": false
@@ -904,6 +906,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
"href": "project-wikis-test-file",
"target": "_blank",
"class": null,
+ "uploading": false,
"title": null,
"canonicalSrc": "project-wikis-test-file",
"isReference": false