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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/playable.js11
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue5
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue8
-rw-r--r--app/assets/javascripts/boards/config_toggle.js3
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/audio.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js7
-rw-r--r--app/assets/javascripts/content_editor/extensions/video.js10
-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/helpers/application_settings_helper.rb2
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--db/migrate/20210817172214_add_yaml_limits_application_setting.rb10
-rw-r--r--db/migrate/20210830154358_add_yaml_limit_constraints.rb25
-rw-r--r--db/schema_migrations/202108171722141
-rw-r--r--db/schema_migrations/202108301543581
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/instance_limits.md23
-rw-r--r--doc/api/discussions.md23
-rw-r--r--doc/api/runners.md39
-rw-r--r--doc/user/compliance/license_compliance/index.md16
-rw-r--r--doc/user/project/merge_requests/index.md15
-rw-r--r--doc/user/project/web_ide/img/open_web_ide.pngbin28571 -> 0 bytes
-rw-r--r--doc/user/project/web_ide/index.md21
-rw-r--r--lib/api/ci/runners.rb50
-rw-r--r--lib/api/entities/ci/reset_registration_token_result.rb11
-rw-r--r--lib/gitlab/config/loader/yaml.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb2
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js2
-rw-r--r--spec/frontend/fixtures/api_markdown.yml9
-rw-r--r--spec/frontend_integration/fly_out_nav_browser_spec.js363
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb18
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb149
35 files changed, 807 insertions, 43 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
index 64bc30e1fe7..2b667aba2d6 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
@@ -1,4 +1,5 @@
/* eslint-disable class-methods-use-this */
+/* eslint-disable @gitlab/require-i18n-strings */
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { Node } from 'tiptap';
@@ -30,19 +31,19 @@ export default class Playable extends Node {
const parseDOM = [
{
- tag: `.media-container`,
+ tag: `.${this.mediaType}-container`,
getAttrs: (el) => ({
- src: el.querySelector('audio,video').src,
- alt: el.querySelector('audio,video').dataset.title,
+ src: el.querySelector(this.mediaType).src,
+ alt: el.querySelector(this.mediaType).dataset.title,
}),
},
];
const toDOM = (node) => [
'span',
- { class: 'media-container' },
+ { class: `media-container ${this.mediaType}-container` },
[
- this.options.mediaType,
+ this.mediaType,
{
src: node.attrs.src,
controls: true,
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 1ca1cf512a1..cfcdd470cf4 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -3,7 +3,6 @@ import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -23,7 +22,7 @@ export default {
import('ee_component/boards/components/board_settings_list_types.vue'),
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['canAdminList'],
+ inject: ['canAdminList', 'scopedLabelsAvailable'],
inheritAttrs: false,
data() {
return {
@@ -61,7 +60,7 @@ export default {
methods: {
...mapActions(['unsetActiveId', 'removeList']),
showScopedLabels(label) {
- return boardsStore.scopedLabels.enabled && isScopedLabel(label);
+ return this.scopedLabelsAvailable && isScopedLabel(label);
},
deleteBoard() {
// eslint-disable-next-line no-alert
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 30e304b8a65..f39e4d90357 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -15,11 +15,6 @@ export default {
},
mixins: [Tracking.mixin()],
props: {
- boardsStore: {
- type: Object,
- required: false,
- default: null,
- },
canAdminList: {
type: Boolean,
required: true,
@@ -41,9 +36,6 @@ export default {
showPage() {
this.track('click_button', { label: 'edit_board' });
eventHub.$emit('showBoardModal', formType.edit);
- if (this.boardsStore) {
- this.boardsStore.showPage(formType.edit);
- }
},
},
};
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js
index 41938d8e284..945a508c55d 100644
--- a/app/assets/javascripts/boards/config_toggle.js
+++ b/app/assets/javascripts/boards/config_toggle.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ConfigToggle from './components/config_toggle.vue';
-export default (boardsStore = undefined) => {
+export default () => {
const el = document.querySelector('.js-board-config');
if (!el) {
@@ -15,7 +15,6 @@ export default (boardsStore = undefined) => {
render(h) {
return h(ConfigToggle, {
props: {
- boardsStore,
canAdminList: parseBoolean(el.dataset.canAdminList),
hasScope: parseBoolean(el.dataset.hasScope),
},
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 8e91718f7b3..7862e5685c9 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -298,7 +298,7 @@ export default () => {
});
}
- boardConfigToggle(boardsStore);
+ boardConfigToggle();
toggleFocusMode();
toggleLabels();
diff --git a/app/assets/javascripts/content_editor/extensions/audio.js b/app/assets/javascripts/content_editor/extensions/audio.js
index 3e746a632df..25d4068c93f 100644
--- a/app/assets/javascripts/content_editor/extensions/audio.js
+++ b/app/assets/javascripts/content_editor/extensions/audio.js
@@ -1,6 +1,7 @@
import Playable from './playable';
export default Playable.extend({
+ name: 'audio',
defaultOptions: {
...Playable.options,
mediaType: 'audio',
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index fdc8f173c81..e9d9ec8905a 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -1,9 +1,10 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
import { Node } from '@tiptap/core';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
export default Node.create({
- name: 'playable',
group: 'inline',
inline: true,
draggable: true,
@@ -46,7 +47,7 @@ export default Node.create({
parseHTML() {
return [
{
- tag: '.media-container',
+ tag: `.${this.options.mediaType}-container`,
},
];
},
@@ -54,7 +55,7 @@ export default Node.create({
renderHTML({ node }) {
return [
'span',
- { class: 'media-container' },
+ { class: `media-container ${this.options.mediaType}-container` },
[
this.options.mediaType,
{
diff --git a/app/assets/javascripts/content_editor/extensions/video.js b/app/assets/javascripts/content_editor/extensions/video.js
new file mode 100644
index 00000000000..9923b7c04cd
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/video.js
@@ -0,0 +1,10 @@
+import Playable from './playable';
+
+export default Playable.extend({
+ name: 'video',
+ defaultOptions: {
+ ...Playable.options,
+ mediaType: 'video',
+ extraElementAttrs: { width: '400' },
+ },
+});
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 368730ed926..85191333739 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -39,6 +39,7 @@ import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
+import Video from '../extensions/video';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
@@ -104,6 +105,7 @@ export const createContentEditor = ({
TaskItem,
TaskList,
Text,
+ Video,
];
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index c21482751b1..d208806b247 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -35,6 +35,7 @@ import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
+import Video from '../extensions/video';
import {
isPlainURL,
renderHardBreak,
@@ -151,6 +152,7 @@ const defaultSerializerConfig = {
else defaultMarkdownSerializer.nodes.bullet_list(state, node);
},
[Text.name]: defaultMarkdownSerializer.nodes.text,
+ [Video.name]: renderPlayable,
},
};
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 8e1fe1e1595..dd8fb1a20b8 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -263,6 +263,8 @@ module ApplicationSettingsHelper
:max_attachment_size,
:max_import_size,
:max_pages_size,
+ :max_yaml_size_bytes,
+ :max_yaml_depth,
:metrics_method_call_threshold,
:minimum_password_length,
:mirror_available,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 0402beb6283..869e9ee5bea 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -343,6 +343,8 @@ class ApplicationSetting < ApplicationRecord
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes }
+ validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true
+ validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true
validates :email_restrictions, untrusted_regexp: true
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 5dfe9f922fd..c194bafeee3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -109,6 +109,8 @@ module ApplicationSettingImplementation
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
max_import_size: 0,
+ max_yaml_size_bytes: 1.megabyte,
+ max_yaml_depth: 100,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
notes_create_limit: 300,
diff --git a/db/migrate/20210817172214_add_yaml_limits_application_setting.rb b/db/migrate/20210817172214_add_yaml_limits_application_setting.rb
new file mode 100644
index 00000000000..f502ef9825b
--- /dev/null
+++ b/db/migrate/20210817172214_add_yaml_limits_application_setting.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddYamlLimitsApplicationSetting < ActiveRecord::Migration[6.1]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :max_yaml_size_bytes, :bigint, default: 1.megabyte, null: false
+ add_column :application_settings, :max_yaml_depth, :integer, default: 100, null: false
+ end
+end
diff --git a/db/migrate/20210830154358_add_yaml_limit_constraints.rb b/db/migrate/20210830154358_add_yaml_limit_constraints.rb
new file mode 100644
index 00000000000..74236993fff
--- /dev/null
+++ b/db/migrate/20210830154358_add_yaml_limit_constraints.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddYamlLimitConstraints < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ SIZE_CONSTRAINT_NAME = 'app_settings_yaml_max_size_positive'
+ DEPTH_CONSTRAINT_NAME = 'app_settings_yaml_max_depth_positive'
+
+ disable_ddl_transaction!
+
+ def up
+ add_check_constraint :application_settings, 'max_yaml_size_bytes > 0', SIZE_CONSTRAINT_NAME
+ add_check_constraint :application_settings, 'max_yaml_depth > 0', DEPTH_CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint :application_settings, SIZE_CONSTRAINT_NAME
+ remove_check_constraint :application_settings, DEPTH_CONSTRAINT_NAME
+ end
+end
diff --git a/db/schema_migrations/20210817172214 b/db/schema_migrations/20210817172214
new file mode 100644
index 00000000000..5e334c7d690
--- /dev/null
+++ b/db/schema_migrations/20210817172214
@@ -0,0 +1 @@
+d6dd6ce802beeea380e0eb1c564f6a5cbc6d30cb3488a3cb91935e1302a4c387 \ No newline at end of file
diff --git a/db/schema_migrations/20210830154358 b/db/schema_migrations/20210830154358
new file mode 100644
index 00000000000..7486c54c4c5
--- /dev/null
+++ b/db/schema_migrations/20210830154358
@@ -0,0 +1 @@
+04a44d0e261b26cc7f39b81a4c59ea8e4903d6d7bf73c2004b426204db4491bc \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 957dbb7e644..67438b2dbaf 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9606,6 +9606,8 @@ CREATE TABLE application_settings (
encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea,
pypi_package_requests_forwarding boolean DEFAULT true NOT NULL,
+ max_yaml_size_bytes bigint DEFAULT 1048576 NOT NULL,
+ max_yaml_depth integer DEFAULT 100 NOT NULL,
throttle_unauthenticated_files_api_requests_per_period integer DEFAULT 125 NOT NULL,
throttle_unauthenticated_files_api_period_in_seconds integer DEFAULT 15 NOT NULL,
throttle_authenticated_files_api_requests_per_period integer DEFAULT 500 NOT NULL,
@@ -9615,6 +9617,8 @@ CREATE TABLE application_settings (
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
+ CONSTRAINT app_settings_yaml_max_depth_positive CHECK ((max_yaml_depth > 0)),
+ CONSTRAINT app_settings_yaml_max_size_positive CHECK ((max_yaml_size_bytes > 0)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 6fb8d2ed80a..9765e6956ad 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -513,6 +513,29 @@ Update `ci_jobs_trace_size_limit` with the new value in megabytes:
Plan.default.actual_limits.update!(ci_jobs_trace_size_limit: 125)
```
+### Maximum size and depth of CI/CD configuration YAML files
+
+The default maximum size of a CI/CD configuration YAML file is 1 megabyte and the default depth is 100.
+
+You can change these limits in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session).
+Update `max_yaml_size_bytes` with the new value in megabytes:
+
+```ruby
+ApplicationSetting.update!(max_yaml_size_bytes: 2.megabytes)
+```
+
+Update `max_yaml_depth` with the new value in megabytes:
+
+```ruby
+ApplicationSetting.update!(max_yaml_depth: 125)
+```
+
+To disable this limitation entirely, disable the feature flag in the console:
+
+```ruby
+Feature.disable(:ci_yaml_limit_size)
+```
+
## Instance monitoring and metrics
### Limit inbound incident management alerts
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 6d15c338f1c..18b74e1450f 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -965,14 +965,31 @@ Parameters for multiline comments only:
#### Line code
-A line code is of the form `<SHA>_<old>_<new>`:
+A line code is of the form `<SHA>_<old>_<new>`, like this: `adc83b19e793491b1c6ea0fd8b46cd9f32e292fc_5_5`
- `<SHA>` is the SHA1 hash of the filename.
- `<old>` is the line number before the change.
- `<new>` is the line number after the change.
-For example, when commenting on an added line number 5, the line code
-looks like `adc83b19e793491b1c6ea0fd8b46cd9f32e292fc_5_5`.
+For example, if a commit (`<COMMIT_ID>`) deletes line 463 in the README, you can comment
+on the deletion by referencing line 463 in the *old* file:
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\
+ --form "note=Very clever to remove this unnecessary line!"\
+ --form "path=README" --form "line=463" --form "line_type=old"\
+ "https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments"
+```
+
+If a commit (`<COMMIT_ID>`) adds line 157 to `hello.rb`, you can comment on the
+addition by referencing line 157 in the *new* file:
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\
+ --form "note=This is brilliant!" --form "path=hello.rb"\
+ --form "line=157" --form "line_type=old"\
+ "https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments"
+```
### Resolve a merge request thread
diff --git a/doc/api/runners.md b/doc/api/runners.md
index c920de26de5..26de946b382 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -673,3 +673,42 @@ Response:
|-----------|---------------------------------|
| 200 | Credentials are valid |
| 403 | Credentials are invalid |
+
+## Reset instance's runner registration token
+
+Resets the runner registration token for the GitLab instance.
+
+```plaintext
+POST /runners/reset_registration_token
+```
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/runners/reset_registration_token"
+```
+
+## Reset project's runner registration token
+
+Resets the runner registration token for a project.
+
+```plaintext
+POST /projects/:id/runners/reset_registration_token
+```
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/9/runners/reset_registration_token"
+```
+
+## Reset group's runner registration token
+
+Resets the runner registration token for a group.
+
+```plaintext
+POST /groups/:id/runners/reset_registration_token
+```
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
+ "https://gitlab.example.com/api/v4/groups/9/runners/reset_registration_token"
+```
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 216a63108a3..7375e03343a 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -53,7 +53,7 @@ You can view and modify existing policies from the [policies](#policies) tab.
The following languages and package managers are supported.
-Java 8 and Gradle 1.x projects are not supported. The minimum supported version of Maven is 3.2.5.
+Gradle 1.x projects are not supported. The minimum supported version of Maven is 3.2.5.
| Language | Package managers | Notes |
|------------|----------------------------------------------------------------------------------------------|-------|
@@ -140,12 +140,12 @@ License Compliance can be configured using CI/CD variables.
| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, Yarn, and npm projects). |
| `ASDF_JAVA_VERSION` | no | Version of Java to use for the scan. |
| `ASDF_NODEJS_VERSION` | no | Version of Node.js to use for the scan. |
-| `ASDF_PYTHON_VERSION` | no | Version of Python to use for the scan. |
+| `ASDF_PYTHON_VERSION` | no | Version of Python to use for the scan. [Configuration](#selecting-the-version-of-python) |
| `ASDF_RUBY_VERSION` | no | Version of Ruby to use for the scan. |
| `GRADLE_CLI_OPTS` | no | Additional arguments for the Gradle executable. If not supplied, defaults to `--exclude-task=test`. |
| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if you have multiple projects in nested directories, you can update your `.gitlab-ci-yml` template to specify a recursive scan, like `LICENSE_FINDER_CLI_OPTS: '--recursive'`. |
-| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
-| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
+| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. [Configuration](#selecting-the-version-of-java) |
+| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. [Configuration](#selecting-the-version-of-python) |
| `MAVEN_CLI_OPTS` | no | Additional arguments for the `mvn` executable. If not supplied, defaults to `-DskipTests`. |
| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
@@ -245,6 +245,12 @@ Alternatively, you can use a Java key store to verify the TLS connection. For in
generate a key store file, see the
[Maven Guide to Remote repository access through authenticated HTTPS](http://maven.apache.org/guides/mini/guide-repository-ssl.html).
+### Selecting the version of Java
+
+License Compliance uses Java 8 by default. You can specify a different Java version using `LM_JAVA_VERSION`.
+
+`LM_JAVA_VERSION` only accepts versions: 8, 11, 14, 15.
+
### Selecting the version of Python
> - [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/-/merge_requests/36) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
@@ -264,6 +270,8 @@ license_scanning:
LM_PYTHON_VERSION: 2
```
+`LM_PYTHON_VERSION` or `ASDF_PYTHON_VERSION` can be used to specify the desired version of Python. When both variables are specified `LM_PYTHON_VERSION` takes precedence.
+
### Custom root certificates for Python
You can supply a custom root certificate to complete TLS verification by using the
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 19b93aa99ff..b0c2b19416b 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -76,6 +76,21 @@ can assign, categorize, and track progress on a merge request:
- [**Notifications**](../../profile/notifications.md): A toggle to select whether
or not to receive notifications for updates to a merge request.
+## Add changes to a merge request
+
+If you have permission to add changes to a merge request, you can add your changes
+to an existing merge request in several ways, depending on the complexity of your change and whether you need access to a development environment:
+
+- [Edit changes in the Web IDE](../web_ide/index.md) in your browser. Use this
+ browser-based method to edit multiple files, or if you are not comfortable with Git commands.
+ You cannot run tests from the Web IDE.
+- [Edit changes in Gitpod](../../../integration/gitpod.md#launch-gitpod-in-gitlab), if you
+ need a fully-featured environment to both edit files, and run tests afterward. Gitpod
+ supports running the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit).
+ To use Gitpod, you must [enable Gitpod in your user account](../../../integration/gitpod.md#enable-gitpod-in-your-user-settings).
+- [Push changes from the command line](../../../gitlab-basics/start-using-git.md), if you are
+ familiar with Git and the command line.
+
## Close a merge request
If you decide to permanently stop work on a merge request,
diff --git a/doc/user/project/web_ide/img/open_web_ide.png b/doc/user/project/web_ide/img/open_web_ide.png
deleted file mode 100644
index 02a5a564472..00000000000
--- a/doc/user/project/web_ide/img/open_web_ide.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index c2b52f9878f..c5ce9364f0e 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -16,9 +16,22 @@ projects by providing an advanced editor with commit staging.
## Open the Web IDE
You can open the Web IDE when viewing a file, from the repository file list,
-and from merge requests.
-
-![Open Web IDE](img/open_web_ide.png)
+and from merge requests:
+
+- *When viewing a file, or the repository file list* -
+ 1. In the upper right corner of the page, select **Edit in Web IDE** if it is visible.
+ 1. If **Edit in Web IDE** is not visible:
+ 1. Select the **(angle-down)** next to **Edit** or **Gitpod**, depending on your configuration.
+ 1. Select **Edit in Web IDE** from the list to display it as the editing option.
+ 1. Select **Edit in Web IDE** to open the editor.
+- *When viewing a merge request* -
+ 1. Go to your merge request, and select the **Overview** tab.
+ 1. Scroll to the widgets area, after the merge request description.
+ 1. Select **Edit in Web IDE** if it is visible.
+ 1. If **Edit in Web IDE** is not visible:
+ 1. Select the **(angle-down)** next to **Open in Gitpod**.
+ 1. Select **Open in Web IDE** from the list to display it as the editing option.
+ 1. Select **Open in Web IDE** to open the editor.
## File finder
@@ -249,7 +262,7 @@ The image is uploaded to the same directory and is named `image.png` by default.
If another file already exists with the same name, a numeric suffix is automatically
added to the filename.
-There are two ways to preview Markdown content in the Web IDE:
+There are two ways to preview Markdown content in the Web IDE:
1. At the top of the file's tab, select **Preview Markdown** to preview the formatting
in your file. You can't edit the file in this view.
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index 7f755b1a4d4..93a40925c21 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -222,6 +222,56 @@ module API
end
end
+ resource :runners do
+ before { authenticate_non_get! }
+
+ desc 'Resets runner registration token' do
+ success Entities::Ci::ResetRegistrationTokenResult
+ end
+ post 'reset_registration_token' do
+ authorize! :update_runners_registration_token
+
+ ApplicationSetting.current.reset_runners_registration_token!
+ present ApplicationSetting.current_without_cache.runners_registration_token, with: Entities::Ci::ResetRegistrationTokenResult
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before { authenticate_non_get! }
+
+ desc 'Resets runner registration token' do
+ success Entities::Ci::ResetRegistrationTokenResult
+ end
+ post ':id/runners/reset_registration_token' do
+ project = find_project! user_project.id
+ authorize! :update_runners_registration_token, project
+
+ project.reset_runners_token!
+ present project.runners_token, with: Entities::Ci::ResetRegistrationTokenResult
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before { authenticate_non_get! }
+
+ desc 'Resets runner registration token' do
+ success Entities::Ci::ResetRegistrationTokenResult
+ end
+ post ':id/runners/reset_registration_token' do
+ group = find_group! user_group.id
+ authorize! :update_runners_registration_token, group
+
+ group.reset_runners_token!
+ present group.runners_token, with: Entities::Ci::ResetRegistrationTokenResult
+ end
+ end
+
helpers do
def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
return runners unless scope.present?
diff --git a/lib/api/entities/ci/reset_registration_token_result.rb b/lib/api/entities/ci/reset_registration_token_result.rb
new file mode 100644
index 00000000000..23426432f68
--- /dev/null
+++ b/lib/api/entities/ci/reset_registration_token_result.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class ResetRegistrationTokenResult < Grape::Entity
+ expose(:token) {|object| object}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb
index 80c9abecd8e..f3a3818f010 100644
--- a/lib/gitlab/config/loader/yaml.rb
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -9,9 +9,6 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
- MAX_YAML_SIZE = 1.megabyte
- MAX_YAML_DEPTH = 100
-
def initialize(config, additional_permitted_classes: [])
@config = YAML.safe_load(config,
permitted_classes: [Symbol, *additional_permitted_classes],
@@ -52,8 +49,8 @@ module Gitlab
def deep_size
strong_memoize(:deep_size) do
Gitlab::Utils::DeepSize.new(@config,
- max_size: MAX_YAML_SIZE,
- max_depth: MAX_YAML_DEPTH)
+ max_size: Gitlab::CurrentSettings.current_application_settings.max_yaml_size_bytes,
+ max_depth: Gitlab::CurrentSettings.current_application_settings.max_yaml_depth)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
index b9447fd929a..bfcc49885a0 100644
--- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
@@ -44,7 +44,7 @@ module QA
end
with_them do
- it "pulls an image using the dependency proxy", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1922' do
+ it "pulls an image using the dependency proxy" do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 60d4d1cb8e3..642ffecff2d 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -1,4 +1,3 @@
-import '~/boards/models/list';
import { GlDrawer, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
@@ -44,6 +43,7 @@ describe('BoardSettingsSidebar', () => {
store,
provide: {
canAdminList,
+ scopedLabelsAvailable: false,
},
}),
);
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 1a6eebc0d4f..feede5458c7 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -173,13 +173,16 @@
Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
- name: audio
markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
-- name: audio_in_lists
+- name: video
+ markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
+- name: audio_and_video_in_lists
markdown: |-
* ![Sample Audio](https://gitlab.com/1.mp3)
- * ![Sample Audio](https://gitlab.com/2.mp3)
+ * ![Sample Video](https://gitlab.com/2.mp4)
- 1. ![Sample Audio](https://gitlab.com/1.mp3)
+ 1. ![Sample Video](https://gitlab.com/1.mp4)
2. ![Sample Audio](https://gitlab.com/2.mp3)
* [x] ![Sample Audio](https://gitlab.com/1.mp3)
* [x] ![Sample Audio](https://gitlab.com/2.mp3)
+ * [x] ![Sample Video](https://gitlab.com/3.mp4)
diff --git a/spec/frontend_integration/fly_out_nav_browser_spec.js b/spec/frontend_integration/fly_out_nav_browser_spec.js
new file mode 100644
index 00000000000..ef2afa20528
--- /dev/null
+++ b/spec/frontend_integration/fly_out_nav_browser_spec.js
@@ -0,0 +1,363 @@
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
+import {
+ calculateTop,
+ showSubLevelItems,
+ canShowSubItems,
+ canShowActiveSubItems,
+ mouseEnterTopItems,
+ mouseLeaveTopItem,
+ getOpenMenu,
+ setOpenMenu,
+ mousePos,
+ getHideSubItemsInterval,
+ documentMouseMove,
+ getHeaderHeight,
+ setSidebar,
+ subItemsMouseLeave,
+} from '~/fly_out_nav';
+
+describe('Fly out sidebar navigation', () => {
+ let el;
+ let breakpointSize = 'lg';
+
+ const OLD_SIDEBAR_WIDTH = 200;
+ const CONTAINER_INITIAL_BOUNDING_RECT = {
+ x: 8,
+ y: 8,
+ width: 769,
+ height: 0,
+ top: 8,
+ right: 777,
+ bottom: 8,
+ left: 8,
+ };
+ const SUB_ITEMS_INITIAL_BOUNDING_RECT = {
+ x: 148,
+ y: 8,
+ width: 0,
+ height: 150,
+ top: 8,
+ right: 148,
+ bottom: 158,
+ left: 148,
+ };
+ const mockBoundingClientRect = (elem, rect) => {
+ jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect);
+ };
+
+ const findSubItems = () => document.querySelector('.sidebar-sub-level-items');
+ const mockBoundingRects = () => {
+ const subItems = findSubItems();
+ mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT);
+ mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT);
+ };
+ const mockSidebarFragment = (styleProps = '') =>
+ `<div class="sidebar-sub-level-items" style="${styleProps}"></div>`;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.style.position = 'relative';
+ document.body.appendChild(el);
+
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ breakpointSize = 'lg';
+ mousePos.length = 0;
+
+ setSidebar(null);
+ });
+
+ describe('calculateTop', () => {
+ it('returns boundingRect top', () => {
+ const boundingRect = {
+ top: 100,
+ height: 100,
+ };
+
+ expect(calculateTop(boundingRect, 100)).toBe(100);
+ });
+ });
+
+ describe('getHideSubItemsInterval', () => {
+ beforeEach(() => {
+ el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;');
+ mockBoundingRects();
+ });
+
+ it('returns 0 if currentOpenMenu is nil', () => {
+ setOpenMenu(null);
+ expect(getHideSubItemsInterval()).toBe(0);
+ });
+
+ it('returns 0 if mousePos is empty', () => {
+ expect(getHideSubItemsInterval()).toBe(0);
+ });
+
+ it('returns 0 when mouse above sub-items', () => {
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top - 50,
+ });
+
+ expect(getHideSubItemsInterval()).toBe(0);
+ });
+
+ it('returns 0 when mouse is below sub-items', () => {
+ const subItems = findSubItems();
+
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50,
+ });
+
+ expect(getHideSubItemsInterval()).toBe(0);
+ });
+
+ it('returns 300 when mouse is moved towards sub-items', () => {
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left + 20,
+ clientY: el.getBoundingClientRect().top + 10,
+ });
+
+ expect(getHideSubItemsInterval()).toBe(300);
+ });
+ });
+
+ describe('mouseLeaveTopItem', () => {
+ beforeEach(() => {
+ jest.spyOn(el.classList, 'remove');
+ });
+
+ it('removes is-over class if currentOpenMenu is null', () => {
+ setOpenMenu(null);
+
+ mouseLeaveTopItem(el);
+
+ expect(el.classList.remove).toHaveBeenCalledWith('is-over');
+ });
+
+ it('removes is-over class if currentOpenMenu is null & there are sub-items', () => {
+ setOpenMenu(null);
+ el.innerHTML = mockSidebarFragment('position: absolute');
+
+ mouseLeaveTopItem(el);
+
+ expect(el.classList.remove).toHaveBeenCalledWith('is-over');
+ });
+
+ it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => {
+ setOpenMenu(null);
+ el.innerHTML = mockSidebarFragment('position: absolute');
+
+ setOpenMenu(findSubItems());
+ mouseLeaveTopItem(el);
+
+ expect(el.classList.remove).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('mouseEnterTopItems', () => {
+ beforeEach(() => {
+ el.innerHTML = mockSidebarFragment(
+ `position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`,
+ );
+ mockBoundingRects();
+ });
+
+ it('shows sub-items after 0ms if no menu is open', (done) => {
+ const subItems = findSubItems();
+ mouseEnterTopItems(el);
+
+ expect(getHideSubItemsInterval()).toBe(0);
+
+ setTimeout(() => {
+ expect(subItems.style.display).toBe('block');
+ done();
+ });
+ });
+
+ it('shows sub-items after 300ms if a menu is currently open', (done) => {
+ const subItems = findSubItems();
+
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+
+ setOpenMenu(subItems);
+
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left + 20,
+ clientY: el.getBoundingClientRect().top + 10,
+ });
+
+ mouseEnterTopItems(el, 0);
+
+ setTimeout(() => {
+ expect(subItems.style.display).toBe('block');
+
+ done();
+ });
+ });
+ });
+
+ describe('showSubLevelItems', () => {
+ beforeEach(() => {
+ el.innerHTML = mockSidebarFragment('position: absolute');
+ });
+
+ it('adds is-over class to el', () => {
+ jest.spyOn(el.classList, 'add');
+
+ showSubLevelItems(el);
+
+ expect(el.classList.add).toHaveBeenCalledWith('is-over');
+ });
+
+ it('does not show sub-items on mobile', () => {
+ breakpointSize = 'xs';
+
+ showSubLevelItems(el);
+
+ expect(findSubItems().style.display).not.toBe('block');
+ });
+
+ it('shows sub-items', () => {
+ showSubLevelItems(el);
+
+ expect(findSubItems().style.display).toBe('block');
+ });
+
+ it('shows collapsed only sub-items if icon only sidebar', () => {
+ const subItems = findSubItems();
+ const sidebar = document.createElement('div');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
+ subItems.classList.add('is-fly-out-only');
+
+ setSidebar(sidebar);
+
+ showSubLevelItems(el);
+
+ expect(findSubItems().style.display).toBe('block');
+ });
+
+ it('does not show collapsed only sub-items if icon only sidebar', () => {
+ const subItems = findSubItems();
+ subItems.classList.add('is-fly-out-only');
+
+ showSubLevelItems(el);
+
+ expect(subItems.style.display).not.toBe('block');
+ });
+
+ it('sets transform of sub-items', () => {
+ const sidebar = document.createElement('div');
+ const subItems = findSubItems();
+
+ sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`;
+
+ document.body.appendChild(sidebar);
+
+ setSidebar(sidebar);
+ showSubLevelItems(el);
+
+ expect(subItems.style.transform).toBe(
+ `translate3d(${OLD_SIDEBAR_WIDTH}px, ${
+ Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()
+ }px, 0)`,
+ );
+ });
+
+ it('sets is-above when element is above', () => {
+ const subItems = findSubItems();
+ mockBoundingRects();
+
+ subItems.style.height = `${window.innerHeight + el.offsetHeight}px`;
+ el.style.top = `${window.innerHeight - el.offsetHeight}px`;
+
+ jest.spyOn(subItems.classList, 'add');
+
+ showSubLevelItems(el);
+
+ expect(subItems.classList.add).toHaveBeenCalledWith('is-above');
+ });
+ });
+
+ describe('canShowSubItems', () => {
+ it('returns true if on desktop size', () => {
+ expect(canShowSubItems()).toBeTruthy();
+ });
+
+ it('returns false if on mobile size', () => {
+ breakpointSize = 'xs';
+
+ expect(canShowSubItems()).toBeFalsy();
+ });
+ });
+
+ describe('canShowActiveSubItems', () => {
+ it('returns true by default', () => {
+ expect(canShowActiveSubItems(el)).toBeTruthy();
+ });
+
+ it('returns false when active & expanded sidebar', () => {
+ const sidebar = document.createElement('div');
+ el.classList.add('active');
+
+ setSidebar(sidebar);
+
+ expect(canShowActiveSubItems(el)).toBeFalsy();
+ });
+
+ it('returns true when active & collapsed sidebar', () => {
+ const sidebar = document.createElement('div');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
+ el.classList.add('active');
+
+ setSidebar(sidebar);
+
+ expect(canShowActiveSubItems(el)).toBeTruthy();
+ });
+ });
+
+ describe('subItemsMouseLeave', () => {
+ beforeEach(() => {
+ el.innerHTML = mockSidebarFragment('position: absolute');
+
+ setOpenMenu(findSubItems());
+ });
+
+ it('hides subMenu if element is not hovered', () => {
+ subItemsMouseLeave(el);
+
+ expect(getOpenMenu()).toBeNull();
+ });
+
+ it('does not hide subMenu if element is hovered', () => {
+ el.classList.add('is-over');
+ subItemsMouseLeave(el);
+
+ expect(getOpenMenu()).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 731ee12d7f4..be568a8e5f9 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -15,6 +15,24 @@ RSpec.describe Gitlab::Config::Loader::Yaml do
YAML
end
+ context 'when max yaml size and depth are set in ApplicationSetting' do
+ let(:yaml_size) { 2.megabytes }
+ let(:yaml_depth) { 200 }
+
+ before do
+ stub_application_setting(max_yaml_size_bytes: yaml_size, max_yaml_depth: yaml_depth)
+ end
+
+ it 'uses ApplicationSetting values rather than the defaults' do
+ expect(Gitlab::Utils::DeepSize)
+ .to receive(:new)
+ .with(any_args, { max_size: yaml_size, max_depth: yaml_depth })
+ .and_call_original
+
+ loader.load!
+ end
+ end
+
context 'when yaml syntax is correct' do
let(:yml) { 'image: ruby:2.7' }
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 93b37b455fd..71a609d8a26 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -79,6 +79,10 @@ RSpec.describe ApplicationSetting do
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
it { is_expected.to validate_presence_of(:max_artifacts_size) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_presence_of(:max_yaml_size_bytes) }
+ it { is_expected.to validate_numericality_of(:max_yaml_size_bytes).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_presence_of(:max_yaml_depth) }
+ it { is_expected.to validate_numericality_of(:max_yaml_depth).only_integer.is_greater_than(0) }
it { is_expected.to validate_presence_of(:max_pages_size) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
is_expected.to validate_numericality_of(:max_pages_size).only_integer.is_greater_than_or_equal_to(0)
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
new file mode 100644
index 00000000000..7623d3f1b17
--- /dev/null
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ci::Runners do
+ subject { post api("#{prefix}/runners/reset_registration_token", user) }
+
+ shared_examples 'bad request' do |result|
+ it 'returns 400 error' do
+ expect { subject }.not_to change { get_token }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq(result)
+ end
+ end
+
+ shared_examples 'unauthenticated' do
+ it 'returns 401 error' do
+ expect { subject }.not_to change { get_token }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ shared_examples 'unauthorized' do
+ it 'returns 403 error' do
+ expect { subject }.not_to change { get_token }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples 'not found' do |scope|
+ it 'returns 404 error' do
+ expect { subject }.not_to change { get_token }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response).to eq({ 'message' => "404 #{scope.capitalize} Not Found" })
+ end
+ end
+
+ shared_context 'when unauthorized' do |scope|
+ context 'when unauthorized' do
+ let_it_be(:user) { create(:user) }
+
+ context "when not a #{scope} member" do
+ it_behaves_like 'not found', scope
+ end
+
+ context "with a non-admin #{scope} member" do
+ before do
+ target.add_developer(user)
+ end
+
+ it_behaves_like 'unauthorized'
+ end
+ end
+ end
+
+ shared_context 'when authorized' do |scope|
+ it 'resets runner registration token' do
+ expect { subject }.to change { get_token }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq({ 'token' => get_token })
+ end
+
+ if scope != 'instance'
+ context 'when malformed id is provided' do
+ let(:prefix) { "/#{scope.pluralize}/some%20string" }
+
+ it_behaves_like 'not found', scope
+ end
+ end
+ end
+
+ describe '/api/v4/runners/reset_registration_token' do
+ describe 'POST /api/v4/runners/reset_registration_token' do
+ before do
+ ApplicationSetting.create_from_defaults
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ let(:prefix) { '' }
+
+ context 'when unauthenticated' do
+ let(:user) { nil }
+
+ it_behaves_like 'unauthenticated'
+ end
+
+ context 'when unauthorized' do
+ let(:user) { create(:user) }
+
+ context "with a non-admin instance member" do
+ it_behaves_like 'unauthorized'
+ end
+ end
+
+ include_context 'when authorized', 'instance' do
+ let_it_be(:user) { create(:user, :admin) }
+
+ def get_token
+ ApplicationSetting.current_without_cache.runners_registration_token
+ end
+ end
+ end
+ end
+
+ describe '/api/v4/groups/:id/runners/reset_registration_token' do
+ describe 'POST /api/v4/groups/:id/runners/reset_registration_token' do
+ let_it_be(:group) { create_default(:group, :private) }
+
+ let(:prefix) { "/groups/#{group.id}" }
+
+ include_context 'when unauthorized', 'group' do
+ let(:target) { group }
+ end
+
+ include_context 'when authorized', 'group' do
+ let_it_be(:user) { create_default(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ def get_token
+ group.reload.runners_token
+ end
+ end
+ end
+ end
+
+ describe '/api/v4/projects/:id/runners/reset_registration_token' do
+ describe 'POST /api/v4/projects/:id/runners/reset_registration_token' do
+ let_it_be(:project) { create_default(:project) }
+
+ let(:prefix) { "/projects/#{project.id}" }
+
+ include_context 'when unauthorized', 'project' do
+ let(:target) { project }
+ end
+
+ include_context 'when authorized', 'project' do
+ let_it_be(:user) { project.owner }
+
+ def get_token
+ project.reload.runners_token
+ end
+ end
+ end
+ end
+end