diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-05 21:21:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-05 21:21:08 +0300 |
commit | 534ce3b2d0a6ec24de9c370e5b85c9528ff63e34 (patch) | |
tree | 4ad964818b181fddc0925e33b63f9b1f2ded23d3 | |
parent | 4ba8ae97071935c39216afc53304c60386bbfa68 (diff) |
Add latest changes from gitlab-org/gitlab@master
107 files changed, 1101 insertions, 529 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index dac452cb484..dd43221d20e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -994,7 +994,7 @@ Gemspec/AvoidExecutingGit: Lint/BinaryOperatorWithIdenticalOperands: Exclude: - - '{,ee/,qa/}spec/**/*_{spec,shared_examples,shared_context}.rb' + - '{,ee/,qa/,jh/}spec/**/*_{spec,shared_examples,shared_context}.rb' Cop/SidekiqRedisCall: Enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index 3cbaf38ed71..69a3a0356cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,6 +67,7 @@ PATH remote: gems/gitlab-safe_request_store specs: gitlab-safe_request_store (0.1.0) + rack (~> 2.2.8) request_store PATH diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 9ee4f7cf4aa..17744e2c6ab 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -46,7 +46,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { let hasPlainText; formTextarea.wrap('<div class="div-dropzone"></div>'); - formTextarea.on('paste', (event) => handlePaste(event)); // Add dropzone area to the form. const $mdArea = formTextarea.closest('.md-area'); @@ -60,6 +59,8 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { return null; } + formTextarea.on('paste', (event) => handlePaste(event)); + const dropzone = $formDropzone.dropzone({ url: uploadsPath, dictDefaultMessage: '', diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue index ee50afd3613..3a2b786dbae 100644 --- a/app/assets/javascripts/organizations/new/components/app.vue +++ b/app/assets/javascripts/organizations/new/components/app.vue @@ -42,7 +42,12 @@ export default { } = await this.$apollo.mutate({ mutation: organizationCreateMutation, variables: { - input: { name: formValues.name, path: formValues.path, avatar: formValues.avatar }, + input: { + name: formValues.name, + path: formValues.path, + description: formValues.description, + avatar: formValues.avatar, + }, }, context: { hasUpload: formValues.avatar instanceof File, diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js index 9c7e5344800..563e366b2c6 100644 --- a/app/assets/javascripts/organizations/new/index.js +++ b/app/assets/javascripts/organizations/new/index.js @@ -13,7 +13,9 @@ export const initOrganizationsNew = () => { const { dataset: { appData }, } = el; - const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData)); + const { organizationsPath, rootUrl, previewMarkdownPath } = convertObjectPropsToCamelCase( + JSON.parse(appData), + ); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -26,6 +28,7 @@ export const initOrganizationsNew = () => { provide: { organizationsPath, rootUrl, + previewMarkdownPath, }, render(createElement) { return createElement(App); diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue index 283a652f90e..1cea2ceeb90 100644 --- a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue +++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue @@ -6,6 +6,7 @@ import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; import { FORM_FIELD_NAME, FORM_FIELD_ID, + FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR, } from '~/organizations/shared/constants'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; @@ -29,7 +30,7 @@ export default { ), successMessage: s__('Organization|Organization was successfully updated.'), }, - fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR], + fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR], data() { return { loading: false, @@ -66,6 +67,7 @@ export default { input: { id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id), name: formValues.name, + description: formValues.description, ...this.avatarInput(formValues), }, }, diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js index 138606a0aab..3ac1243ff0f 100644 --- a/app/assets/javascripts/organizations/settings/general/index.js +++ b/app/assets/javascripts/organizations/settings/general/index.js @@ -13,9 +13,12 @@ export const initOrganizationsSettingsGeneral = () => { const { dataset: { appData }, } = el; - const { organization, organizationsPath, rootUrl } = convertObjectPropsToCamelCase( - JSON.parse(appData), - ); + const { + organization, + organizationsPath, + rootUrl, + previewMarkdownPath, + } = convertObjectPropsToCamelCase(JSON.parse(appData)); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -29,6 +32,7 @@ export const initOrganizationsSettingsGeneral = () => { organization, organizationsPath, rootUrl, + previewMarkdownPath, }, render(createElement) { return createElement(App); diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue index 3567fa490ea..49519369e9a 100644 --- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue +++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue @@ -4,10 +4,13 @@ import { formValidators } from '@gitlab/ui/dist/utils'; import { s__, __ } from '~/locale'; import { slugify } from '~/lib/utils/text_utility'; import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH, + FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR, FORM_FIELD_PATH_VALIDATORS, } from '../constants'; @@ -21,12 +24,27 @@ export default { GlButton, OrganizationUrlField, AvatarUploadDropzone, + MarkdownField, }, i18n: { cancel: __('Cancel'), }, formId: 'new-organization-form', - inject: ['organizationsPath'], + markdownDocsPath: helpPagePath('user/organization/index', { + anchor: 'organization-description-supported-markdown', + }), + restrictedToolBarItems: [ + 'code', + 'quote', + 'bullet-list', + 'numbered-list', + 'task-list', + 'collapsible-section', + 'table', + 'attach-file', + 'full-screen', + ], + inject: ['organizationsPath', 'previewMarkdownPath'], props: { loading: { type: Boolean, @@ -39,6 +57,7 @@ export default { return { [FORM_FIELD_NAME]: '', [FORM_FIELD_PATH]: '', + [FORM_FIELD_DESCRIPTION]: '', [FORM_FIELD_AVATAR]: null, }; }, @@ -47,7 +66,7 @@ export default { type: Array, required: false, default() { - return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_AVATAR]; + return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR]; }, }, submitButtonText: { @@ -102,6 +121,12 @@ export default { class: 'gl-w-full', }, }, + [FORM_FIELD_DESCRIPTION]: { + label: s__('Organization|Organization description (optional)'), + groupAttrs: { + class: 'gl-w-full common-note-form', + }, + }, [FORM_FIELD_AVATAR]: { label: s__('Organization|Organization avatar'), groupAttrs: { @@ -137,6 +162,7 @@ export default { formFieldsInputEvent(event); this.hasPathBeenManuallySet = true; }, + helpPagePath, }, }; </script> @@ -159,6 +185,28 @@ export default { @blur="blur" /> </template> + <template #input(description)="{ id, value, input, blur }"> + <div class="gl-md-form-input-xl"> + <markdown-field + :can-attach-file="false" + :markdown-preview-path="previewMarkdownPath" + :markdown-docs-path="$options.markdownDocsPath" + :textarea-value="value" + :restricted-tool-bar-items="$options.restrictedToolBarItems" + > + <template #textarea> + <textarea + :id="id" + :value="value" + class="note-textarea js-gfm-input markdown-area" + maxlength="1024" + @input="input($event.target.value)" + @blur="blur" + ></textarea> + </template> + </markdown-field> + </div> + </template> <template #input(avatar)="{ input, value }"> <avatar-upload-dropzone :value="value" @@ -169,9 +217,14 @@ export default { </template> </gl-form-fields> <div class="gl-display-flex gl-gap-3"> - <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{ - submitButtonText - }}</gl-button> + <gl-button + type="submit" + variant="confirm" + class="js-no-auto-disable" + :loading="loading" + data-testid="submit-button" + >{{ submitButtonText }}</gl-button + > <gl-button v-if="showCancelButton" :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button> diff --git a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue index d36f62477e6..c4b31e6b8a6 100644 --- a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue +++ b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue @@ -39,7 +39,7 @@ export default { </script> <template> - <gl-form-input-group> + <gl-form-input-group class="gl-md-form-input-xl"> <template #prepend> <gl-input-group-text class="organization-root-path"> <gl-truncate :text="baseUrl" position="middle" /> @@ -50,7 +50,7 @@ export default { :id="id" :value="value" :placeholder="$options.i18n.pathPlaceholder" - class="gl-h-auto! gl-md-form-input-lg" + class="gl-h-auto!" @input="$emit('input', $event)" @blur="$emit('blur', $event)" /> diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js index 9f3d066f984..c4f4f348ff2 100644 --- a/app/assets/javascripts/organizations/shared/constants.js +++ b/app/assets/javascripts/organizations/shared/constants.js @@ -4,6 +4,7 @@ import { s__ } from '~/locale'; export const FORM_FIELD_NAME = 'name'; export const FORM_FIELD_ID = 'id'; export const FORM_FIELD_PATH = 'path'; +export const FORM_FIELD_DESCRIPTION = 'description'; export const FORM_FIELD_AVATAR = 'avatar'; export const FORM_FIELD_PATH_VALIDATORS = [ diff --git a/app/assets/javascripts/organizations/show/components/app.vue b/app/assets/javascripts/organizations/show/components/app.vue index 47264d80454..4cd5ada1d66 100644 --- a/app/assets/javascripts/organizations/show/components/app.vue +++ b/app/assets/javascripts/organizations/show/components/app.vue @@ -1,11 +1,12 @@ <script> import OrganizationAvatar from './organization_avatar.vue'; +import OrganizationDescription from './organization_description.vue'; import GroupsAndProjects from './groups_and_projects.vue'; import AssociationCounts from './association_counts.vue'; export default { name: 'OrganizationShowApp', - components: { OrganizationAvatar, GroupsAndProjects, AssociationCounts }, + components: { OrganizationAvatar, OrganizationDescription, GroupsAndProjects, AssociationCounts }, props: { organization: { type: Object, @@ -26,6 +27,7 @@ export default { <template> <div class="gl-py-6"> <organization-avatar :organization="organization" /> + <organization-description :organization="organization" /> <association-counts :association-counts="associationCounts" :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath" diff --git a/app/assets/javascripts/organizations/show/components/organization_description.vue b/app/assets/javascripts/organizations/show/components/organization_description.vue new file mode 100644 index 00000000000..6222bdcd082 --- /dev/null +++ b/app/assets/javascripts/organizations/show/components/organization_description.vue @@ -0,0 +1,24 @@ +<script> +import SafeHtml from '~/vue_shared/directives/safe_html'; + +export default { + name: 'OrganizationDescription', + directives: { + SafeHtml, + }, + props: { + organization: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div + v-if="organization.description_html" + v-safe-html="organization.description_html" + class="gl-mt-5 md" + ></div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index cffd8471d18..525f684df86 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -382,6 +382,7 @@ export default { @click="handleQuote" /> <toolbar-button + v-if="!restrictedToolBarItems.includes('code')" v-show="!previewMarkdown" tag="`" tag-block="```" diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index ce8ccb2bc08..cd9bbf45727 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -27,7 +27,5 @@ // JH-only stylesheets @import 'application_jh'; -/* print styles */ -@media print { - @import 'print'; -} +// print styles +@import 'print'; diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss index 5a5f39a4b77..10bf54b4ffb 100644 --- a/app/assets/stylesheets/emoji_sprites.scss +++ b/app/assets/stylesheets/emoji_sprites.scss @@ -7176,7 +7176,7 @@ } .emoji-icon { - background-image: image-url('emoji.png'); + background-image: url('emoji.png'); background-repeat: no-repeat; color: transparent; text-indent: -99em; @@ -7190,7 +7190,7 @@ only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - background-image: image-url('emoji@2x.png'); + background-image: url('emoji@2x.png'); background-size: 860px 840px; } /* stylelint-enable media-feature-name-no-vendor-prefix */ diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss index 6886e751b72..f776328ebdf 100644 --- a/app/assets/stylesheets/fonts.scss +++ b/app/assets/stylesheets/fonts.scss @@ -11,7 +11,7 @@ Usage: font-style: normal; /* stylelint-disable-next-line property-no-unknown */ font-named-instance: 'Regular'; - src: font-url('gitlab-sans/GitLabSans.woff2') format('woff2'); + src: url('gitlab-sans/GitLabSans.woff2') format('woff2'); } @font-face { @@ -21,7 +21,7 @@ Usage: font-style: italic; /* stylelint-disable-next-line property-no-unknown */ font-named-instance: 'Regular'; - src: font-url('gitlab-sans/GitLabSans-Italic.woff2') format('woff2'); + src: url('gitlab-sans/GitLabSans-Italic.woff2') format('woff2'); } /* ------------------------------------------------------- @@ -35,7 +35,7 @@ Usage: font-weight: 100 900; font-display: swap; font-style: normal; - src: font-url('gitlab-mono/GitLabMono.woff2') format('woff2'); + src: url('gitlab-mono/GitLabMono.woff2') format('woff2'); } @font-face { @@ -43,7 +43,7 @@ Usage: font-weight: 100 900; font-display: swap; font-style: italic; - src: font-url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2'); + src: url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2'); } // This isn't the best solution, but we needed a quick fix diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index b948a57ea33..c9ef9349c36 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -225,7 +225,7 @@ $diff-file-header: 41px; width: 15px; position: absolute; top: 0; - background: image-url('swipemode_sprites.gif') 0 3px no-repeat; + background: url('swipemode_sprites.gif') 0 3px no-repeat; } .bottom-handle { @@ -234,7 +234,7 @@ $diff-file-header: 41px; width: 15px; position: absolute; bottom: 0; - background: image-url('swipemode_sprites.gif') 0 -11px no-repeat; + background: url('swipemode_sprites.gif') 0 -11px no-repeat; } } } @@ -272,7 +272,7 @@ $diff-file-header: 41px; left: 12px; height: 10px; width: 276px; - background: image-url('onion_skin_sprites.gif') -4px -20px repeat-x; + background: url('onion_skin_sprites.gif') -4px -20px repeat-x; } .dragger { @@ -282,7 +282,7 @@ $diff-file-header: 41px; top: 0; height: 14px; width: 14px; - background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x; + background: url('onion_skin_sprites.gif') 0 -34px repeat-x; cursor: pointer; } @@ -293,7 +293,7 @@ $diff-file-header: 41px; right: 0; height: 10px; width: 10px; - background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat; + background: url('onion_skin_sprites.gif') -2px 0 no-repeat; } .opaque { @@ -303,7 +303,7 @@ $diff-file-header: 41px; left: 0; height: 10px; width: 10px; - background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat; + background: url('onion_skin_sprites.gif') -2px -10px no-repeat; } } } @@ -770,12 +770,12 @@ table.code { .frame.click-to-comment, .btn-transparent.image-diff-overlay-add-comment { position: relative; - cursor: image-url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + cursor: url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; // Retina cursor - cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, - image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + cursor: image-set(url('illustrations/image_comment_light_cursor.svg') 1x, + url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; .comment-indicator { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8282f6143c2..649ceb95731 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -575,7 +575,7 @@ left: 1rem; width: 1rem; height: 1rem; - mask-image: asset_url('icons-stacked.svg#check'); + mask-image: url('icons-stacked.svg#check'); mask-repeat: no-repeat; mask-size: cover; mask-position: center center; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 9cb264c992b..7dcde5f0b3c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -420,7 +420,7 @@ span.idiff { @include gl-h-5; @include gl-float-left; background-color: $gray-400; - mask-image: asset_url('icons-stacked.svg#doc-versions'); + mask-image: url('icons-stacked.svg#doc-versions'); mask-repeat: no-repeat; mask-size: cover; mask-position: center; diff --git a/app/assets/stylesheets/framework/source_editor.scss b/app/assets/stylesheets/framework/source_editor.scss index a09ab7ed64c..2b597634519 100644 --- a/app/assets/stylesheets/framework/source_editor.scss +++ b/app/assets/stylesheets/framework/source_editor.scss @@ -78,7 +78,7 @@ @include gl-mr-2; @include gl-w-4; @include gl-h-4; - mask-image: asset_url('icons-stacked.svg#link'); + mask-image: url('icons-stacked.svg#link'); mask-repeat: no-repeat; mask-size: cover; mask-position: center; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8153c4d4717..15e794fc347 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -495,7 +495,7 @@ &::after { @include gl-dark-invert-keep-hue; - content: image-url('icon_anchor.svg'); + content: url('icon_anchor.svg'); visibility: hidden; } } diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index 23fa1326881..3fd72904655 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -118,7 +118,7 @@ @include gl-w-5; @include gl-h-5; background-color: rgba($color, 0.3); - mask-image: asset_url('icons-stacked.svg##{$icon}'); + mask-image: url('icons-stacked.svg##{$icon}'); mask-repeat: no-repeat; mask-size: cover; mask-position: center; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 9748983d1ae..f57a8519992 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -225,7 +225,7 @@ ul.related-merge-requests > li gl-emoji { &::after { @include gl-dark-invert-keep-hue; - content: image-url('icon_anchor.svg'); + content: url('icon_anchor.svg'); visibility: hidden; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 483d151adab..30c34c5ef32 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -966,7 +966,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio; .unified-diff-components-diff-note-button { &::before { background-color: $blue-500; - mask-image: asset_url('icons-stacked.svg#comment'); + mask-image: url('icons-stacked.svg#comment'); mask-repeat: no-repeat; mask-size: cover; mask-position: center; diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 315b9c829a7..d6fcfb3461d 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -4,97 +4,99 @@ @import '@gitlab/ui/src/scss/variables'; @import '@gitlab/ui/src/scss/utility-mixins/index'; -.md h1, -.md h2, -.md h3, -.md h4, -.md h5, -.md h6 { - margin-top: 17px; -} +@media print { + .md h1, + .md h2, + .md h3, + .md h4, + .md h5, + .md h6 { + margin-top: 17px; + } -.md h1 { - font-size: 30px; -} + .md h1 { + font-size: 30px; + } -.md h2 { - font-size: 22px; -} + .md h2 { + font-size: 22px; + } -.md h3 { - font-size: 18px; - font-weight: 600; -} + .md h3 { + font-size: 18px; + font-weight: 600; + } -.md { - print-color-adjust: exact; - -webkit-print-color-adjust: exact; + .md { + print-color-adjust: exact; + -webkit-print-color-adjust: exact; - // fix blockquote style in print - blockquote { - &::before { - position: absolute; - top: 0; - left: -4px; - content: ' '; - height: 100%; - width: 4px; - background-color: $gray-100; - } + // fix blockquote style in print + blockquote { + &::before { + position: absolute; + top: 0; + left: -4px; + content: ' '; + height: 100%; + width: 4px; + background-color: $gray-100; + } - position: relative; - font-size: inherit; - @include gl-text-gray-700; - @include gl-py-3; - @include gl-pl-6; - @include gl-my-3; - @include gl-mx-0; - @include gl-inset-border-l-4-gray-100; - margin-left: 4px; - border: 0 !important; + position: relative; + font-size: inherit; + @include gl-text-gray-700; + @include gl-py-3; + @include gl-pl-6; + @include gl-my-3; + @include gl-mx-0; + @include gl-inset-border-l-4-gray-100; + margin-left: 4px; + border: 0 !important; + } } -} -header, -nav, -.nav-sidebar, -.super-sidebar, -.profiler-results, -.tree-ref-holder, -.tree-holder .breadcrumb, -.nav, -.btn, -ul.notes-form, -.issuable-gutter-toggle, -.gutter-toggle, -.issuable-details .content-block-small, -.edit-link, -.note-action-button, -.right-sidebar, -.flash-container, -copy-code, -#js-peek { - display: none !important; -} + header, + nav, + .nav-sidebar, + .super-sidebar, + .profiler-results, + .tree-ref-holder, + .tree-holder .breadcrumb, + .nav, + .btn, + ul.notes-form, + .issuable-gutter-toggle, + .gutter-toggle, + .issuable-details .content-block-small, + .edit-link, + .note-action-button, + .right-sidebar, + .flash-container, + copy-code, + #js-peek { + display: none !important; + } -pre { - page-break-before: avoid; - page-break-inside: auto; -} + pre { + page-break-before: avoid; + page-break-inside: auto; + } -.page-gutter { - padding-top: 0; - padding-left: 0; -} + .page-gutter { + padding-top: 0; + padding-left: 0; + } -.right-sidebar { - top: 0; -} + .right-sidebar { + top: 0; + } -a[href]::after { - content: none !important; -} + a[href]::after { + content: none !important; + } -.with-performance-bar .layout-page { - padding-top: 0; + .with-performance-bar .layout-page { + padding-top: 0; + } } diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss index 91b381462be..e1b14df683e 100644 --- a/app/assets/stylesheets/snippets.scss +++ b/app/assets/stylesheets/snippets.scss @@ -15,8 +15,7 @@ .gl-snippet-icon { display: inline-block; - /* stylelint-disable-next-line function-url-quotes */ - background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat; + background: url('ext_snippet_icons/ext_snippet_icons.png') no-repeat; overflow: hidden; text-align: left; width: 16px; diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 7f1b961e92a..8bd120b5ed5 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -44,6 +44,7 @@ module PreviewMarkdown when 'groups' then { group: group, issuable_reference_expansion_enabled: true } when 'projects' then projects_filter_params when 'timeline_events' then timeline_events_filter_params + when 'organizations' then { pipeline: :description } else {} end.merge( requested_path: params[:path], diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb index 9f09627b1e4..0596441591d 100644 --- a/app/controllers/organizations/organizations_controller.rb +++ b/app/controllers/organizations/organizations_controller.rb @@ -2,9 +2,11 @@ module Organizations class OrganizationsController < ApplicationController + include PreviewMarkdown + feature_category :cell - skip_before_action :authenticate_user!, except: [:index, :new, :users] + skip_before_action :authenticate_user!, only: [:show, :groups_and_projects] def index; end diff --git a/app/finders/ci/runner_jobs_finder.rb b/app/finders/ci/runner_jobs_finder.rb index b659eda6646..91d8eccff21 100644 --- a/app/finders/ci/runner_jobs_finder.rb +++ b/app/finders/ci/runner_jobs_finder.rb @@ -13,7 +13,13 @@ module Ci end def execute - items = @runner.builds + items = if params[:system_id].blank? + runner.builds + else + runner_manager = Ci::RunnerManager.for_runner(runner).with_system_xid(params[:system_id]).first + Ci::Build.belonging_to_runner_manager(runner_manager&.id) + end + items = by_permission(items) items = by_status(items) sort_items(items) diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index b6d39276a03..445dd3a1f6f 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -4,7 +4,8 @@ module Organizations module OrganizationHelper def organization_show_app_data(organization) { - organization: organization.slice(:id, :name).merge({ avatar_url: organization.avatar_url(size: 128) }), + organization: organization.slice(:id, :name, :description_html) + .merge({ avatar_url: organization.avatar_url(size: 128) }), groups_and_projects_organization_path: groups_and_projects_organization_path(organization), # TODO: Update counts to use real data # https://gitlab.com/gitlab-org/gitlab/-/issues/424531 @@ -17,18 +18,14 @@ module Organizations end def organization_new_app_data - { - organizations_path: organizations_path, - root_url: root_url - }.to_json + shared_new_settings_general_app_data.to_json end def organization_settings_general_app_data(organization) { - organization: organization.slice(:id, :name, :path).merge({ avatar: organization.avatar_url(size: 192) }), - organizations_path: organizations_path, - root_url: root_url - }.to_json + organization: organization.slice(:id, :name, :path, :description) + .merge({ avatar: organization.avatar_url(size: 192) }) + }.merge(shared_new_settings_general_app_data).to_json end def organization_groups_and_projects_app_data @@ -66,6 +63,14 @@ module Organizations } end + def shared_new_settings_general_app_data + { + preview_markdown_path: preview_markdown_organizations_path, + organizations_path: organizations_path, + root_url: root_url + } + end + # See UsersHelper#admin_users_paths for inspiration to this method def organizations_users_paths { diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 23185548554..66e9e20f3dc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -190,6 +190,10 @@ module Ci # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123131 scope :with_runner_type, -> (runner_type) { joins(:runner).where(runner: { runner_type: runner_type }) } + scope :belonging_to_runner_manager, -> (runner_machine_id) { + joins(:runner_manager_build).where(p_ci_runner_machine_builds: { runner_machine_id: runner_machine_id }) + } + scope :with_secure_reports_from_config_options, -> (job_types) do joins(:metadata).where("#{Ci::BuildMetadata.quoted_table_name}.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types) end diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index 71005f4341a..44fe1bdd67d 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -55,6 +55,10 @@ module Ci where(runner_id: runner_id) end + scope :with_system_xid, ->(system_xid) do + where(system_xid: system_xid) + end + scope :with_running_builds, -> do where('EXISTS(?)', Ci::Build.select(1) diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index be96939daa8..7f86bf81375 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -30,7 +30,7 @@ module Organizations 'organizations/path': true, length: { minimum: 2, maximum: 255 } - delegate :description, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail + delegate :description, :description_html, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail accepts_nested_attributes_for :organization_detail diff --git a/app/models/organizations/organization_detail.rb b/app/models/organizations/organization_detail.rb index b69ec5eae76..018e7579c5b 100644 --- a/app/models/organizations/organization_detail.rb +++ b/app/models/organizations/organization_detail.rb @@ -6,7 +6,7 @@ module Organizations include Avatarable include WithUploads - cache_markdown_field :description + cache_markdown_field :description, pipeline: :description belongs_to :organization, inverse_of: :organization_detail diff --git a/config/routes/organizations.rb b/config/routes/organizations.rb index 62c791cdf69..dbc9f2ce226 100644 --- a/config/routes/organizations.rb +++ b/config/routes/organizations.rb @@ -6,6 +6,10 @@ resources( param: :organization_path, module: :organizations ) do + collection do + post :preview_markdown + end + member do get :groups_and_projects get :users diff --git a/doc/administration/auth/index.md b/doc/administration/auth/index.md index 95268f6f39b..dd7f9cec77e 100644 --- a/doc/administration/auth/index.md +++ b/doc/administration/auth/index.md @@ -1,6 +1,7 @@ --- stage: Govern group: Authentication +description: Third-party authentication providers. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/administration/configure.md b/doc/administration/configure.md index 8b4f8a9abc6..7ae088cd783 100644 --- a/doc/administration/configure.md +++ b/doc/administration/configure.md @@ -1,6 +1,7 @@ --- stage: Systems group: Distribution +description: Installation settings. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md index 05a241b65dc..c43169250f0 100644 --- a/doc/administration/dedicated/index.md +++ b/doc/administration/dedicated/index.md @@ -1,8 +1,8 @@ --- stage: SaaS Platforms group: GitLab Dedicated -info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments" -description: 'Learn how to configure your GitLab Dedicated instance.' +description: IP allowlists, SAML, maintenance. +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- # Configure GitLab Dedicated **(ULTIMATE)** diff --git a/doc/administration/license.md b/doc/administration/license.md index 1e77d83825d..9b95af28eed 100644 --- a/doc/administration/license.md +++ b/doc/administration/license.md @@ -40,7 +40,7 @@ The subscription is activated. You can use one activation code or license key for multiple self-managed instances if the users on these instances are the same or are a subset of your licensed production instance. This means that if -you have a licensed production instance of GitLab, and other instances with the same list of users, the +you have a licensed production instance of GitLab, and other instances with the same list of users, the production activation code applies, even if these users are configured in different groups and projects. ### Uploading licenses for scaled architectures diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index 33f407b2dbf..d1ec818a2a5 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -1,6 +1,7 @@ --- stage: Systems group: Distribution +description: Backup and restore, move repos, maintenance tasks. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index a9e3f2c1109..7e4e929f80d 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -1,6 +1,7 @@ --- stage: Systems group: Distribution +description: Recommended deployments at scale. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/administration/settings/index.md b/doc/administration/settings/index.md index 531073887c5..2df9d1cd52d 100644 --- a/doc/administration/settings/index.md +++ b/doc/administration/settings/index.md @@ -1,6 +1,7 @@ --- stage: none group: unassigned +description: Product settings. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/runners.md b/doc/api/runners.md index ac854a477d3..373fc4e4344 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -14,6 +14,7 @@ This page describes endpoints for runners registered to an instance. To create a GET /runners GET /runners/all GET /runners/:id/jobs +GET /runners/:id/managers/:system_id/jobs GET /projects/:id/runners GET /groups/:id/runners ``` @@ -367,7 +368,7 @@ NOTE: The `active` form attribute was deprecated [in GitLab 14.8](https://gitlab.com/gitlab-org/gitlab/-/issues/347211). and will be removed in [a future version of the REST API](https://gitlab.com/gitlab-org/gitlab/-/issues/351109). It is replaced by the `paused` attribute. -## List runner's jobs +## List jobs processed by a runner > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15432) in GitLab 10.3. @@ -378,12 +379,13 @@ to projects where the user has at least the Reporter role. GET /runners/:id/jobs ``` -| Attribute | Type | Required | Description | -|-----------|---------|----------|---------------------| -| `id` | integer | yes | The ID of a runner | -| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | -| `order_by`| string | no | Order jobs by `id` | -| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`). Specify `order_by` as well, including for `id`. | +| Attribute | Type | Required | Description | +|-------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a runner | +| `system_id` | string | no | System ID of the machine where the runner manager is running | +| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | +| `order_by` | string | no | Order jobs by `id` | +| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`). If `sort` is specified, `order_by` must be specified as well | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running" diff --git a/doc/architecture/blueprints/cells/routing-service.md b/doc/architecture/blueprints/cells/routing-service.md index 2b4fa88f187..bd5570b68f4 100644 --- a/doc/architecture/blueprints/cells/routing-service.md +++ b/doc/architecture/blueprints/cells/routing-service.md @@ -378,7 +378,7 @@ Each Cell does implement classification endpoint: requests for sharding keys that are not found. - The cached response is for time defined by `expiry` and `refresh`. - The `expiry` defines when the item is removed from cache unless used. - - The `refresh` defines when the item needs to be reclassified if used. + - The `refresh` defines when the item needs to be reclassified if used. - The refresh is done asynchronously as the request should be served without a delay if they were classified. The refresh is done to ensure that cache is always hot and up-to date. For the above example: diff --git a/doc/architecture/blueprints/runner_admission_controller/index.md b/doc/architecture/blueprints/runner_admission_controller/index.md index 21dc1d53303..0a62b271901 100644 --- a/doc/architecture/blueprints/runner_admission_controller/index.md +++ b/doc/architecture/blueprints/runner_admission_controller/index.md @@ -140,7 +140,7 @@ Each runner has a tag identifier unique to that runner, e.g. `DiscoveryOne`, `tu 1. The `preparing` state will wait for a response from the webhook or until timeout. 1. The UI should be updated with the current status of the job prerequisites and admission 1. For jobs where the webhook times out (1 hour) their status should be set as though the admission was denied with a timeout reasoning. This should -be rare in typical circumstances. + be rare in typical circumstances. 1. Jobs with denied admission can be retried. Retried jobs will be resent to the admission controller without tag mutations or runner filtering reset. 1. [`allow_failure`](../../../ci/yaml/index.md#allow_failure) should be updated to support jobs that fail on denied admissions, for example: diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md index f2e9d624d20..c667a460f5c 100644 --- a/doc/architecture/blueprints/runner_tokens/index.md +++ b/doc/architecture/blueprints/runner_tokens/index.md @@ -284,11 +284,11 @@ not an issue per-se. New records are created in 2 situations: -- when the runner calls the `POST /api/v4/runners/verify` endpoint as part of the -`gitlab-runner register` command, if the specified runner token is prefixed with `glrt-`. -This allows the frontend to determine whether the user has successfully completed the registration and take an -appropriate action; -- when GitLab is pinged for new jobs and a record matching the `token`+`system_id` does not already exist. +- When the runner calls the `POST /api/v4/runners/verify` endpoint as part of the + `gitlab-runner register` command, if the specified runner token is prefixed with `glrt-`. + This allows the frontend to determine whether the user has successfully completed the registration and take an + appropriate action; +- When GitLab is pinged for new jobs and a record matching the `token`+`system_id` does not already exist. Due to the time-decaying nature of the `ci_runner_machines` records, they are automatically cleaned after 7 days after the last contact from the respective runner. diff --git a/doc/architecture/blueprints/secret_manager/index.md b/doc/architecture/blueprints/secret_manager/index.md index ac30f3399d8..3a538f58dde 100644 --- a/doc/architecture/blueprints/secret_manager/index.md +++ b/doc/architecture/blueprints/secret_manager/index.md @@ -114,10 +114,10 @@ the data keys mentioned above. ### Further investigations required 1. Management of identities stored in GCP Key Management. -We need to investigate how we can correlate and de-multiplex GitLab identities into -GCP identities that are used to allow access to cryptographic operations on GCP Key Management. + We need to investigate how we can correlate and de-multiplex GitLab identities into + GCP identities that are used to allow access to cryptographic operations on GCP Key Management. 1. Authentication of clients. Clients to the Secrets Manager could be GitLab Runner or external clients. -For each of these, we need a secure and reliable method to authenticate requests to decrypt a secret. + For each of these, we need a secure and reliable method to authenticate requests to decrypt a secret. 1. Assignment of GCP backed private keys to each identity. ### Availability on SaaS and Self-Managed diff --git a/doc/ci/runners/runner_fleet_dashboard.md b/doc/ci/runners/runner_fleet_dashboard.md index 08579f6ff92..ce487ff7386 100644 --- a/doc/ci/runners/runner_fleet_dashboard.md +++ b/doc/ci/runners/runner_fleet_dashboard.md @@ -17,6 +17,8 @@ The Runner Fleet Dashboard shows: - Number of concurrent jobs executed on most busy runners. - Histogram of job queue times [(available only with ClickHouse)](#enable-more-ci-analytics-features-with-clickhouse). +Support for usage and cost analysis are proposed in [epic 11183](https://gitlab.com/groups/gitlab-org/-/epics/11183). + ![Runner Fleet Dashboard](img/runner_fleet_dashboard.png) ## View the Runner Fleet Dashboard @@ -32,7 +34,7 @@ To view the runner fleet dashboard: 1. Click **Fleet dashboard**. Most of the dashboard works without any additional actions, with the -exception of **Wait time to pick a job** chart and [proposed features](#whats-next). +exception of **Wait time to pick a job** chart and features proposed in [epic 11183](https://gitlab.com/groups/gitlab-org/-/epics/11183). These features require [setting up an additional infrastructure](#enable-more-ci-analytics-features-with-clickhouse). ## Enable more CI analytics features with ClickHouse **(ULTIMATE EXPERIMENT)** @@ -41,7 +43,7 @@ These features require [setting up an additional infrastructure](#enable-more-ci This feature is an [Experiment](../../policy/experiment-beta-support.md). To test it, we have launched an early adopters program. -To join the list of users testing this feature, contact us in +To join the list of users testing this feature, see [epic 11180](https://gitlab.com/groups/gitlab-org/-/epics/11180). ### Enable ClickHouse integration and features @@ -53,17 +55,12 @@ To enable additional CI analytics features: | Feature flag name | Purpose | |------------------------------------|---------------------------------------------------------------------------| - | `ci_data_ingestion_to_click_house` | Enables synchronization of new finished CI builds to Clickhouse database. | + | `ci_data_ingestion_to_click_house` | Enables synchronization of new finished CI builds to ClickHouse database. | | `clickhouse_ci_analytics` | Enables the **Wait time to pick a job** chart. | <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For a video walkthrough, see [Setting up Runner Fleet Dashboard with ClickHouse](https://www.youtube.com/watch?v=YpGV95Ctbpk). -### What's next - -Support for usage and cost analysis are proposed in -[epic 11183](https://gitlab.com/groups/gitlab-org/-/epics/11183). - ## Feedback To help us improve the Runner Fleet Dashboard, you can provide feedback in diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 84d2537d058..cad71d4b843 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -376,7 +376,7 @@ Avoid: [_explain why, not what_](https://blog.codinghorror.com/code-tells-you-how-comments-tell-you-why/). - Requesting maintainer reviews of merge requests with failed tests. If the tests are failing and you have to request a review, ensure you leave a comment with an explanation. - Excessively mentioning maintainers through email or Slack (if the maintainer is reachable -through Slack). If you can't add a reviewer for a merge request, `@` mentioning a maintainer in a comment is acceptable and in all other cases adding a reviewer is sufficient. + through Slack). If you can't add a reviewer for a merge request, `@` mentioning a maintainer in a comment is acceptable and in all other cases adding a reviewer is sufficient. This saves reviewers time and helps authors catch mistakes earlier. @@ -412,7 +412,7 @@ that it meets all requirements, you should: - Select **Approve**. - `@` mention the author to generate a to-do notification, and advise them that their merge request has been reviewed and approved. - Request a review from a maintainer. Default to requests for a maintainer with [domain expertise](#domain-experts), -however, if one isn't available or you think the merge request doesn't need a review by a [domain expert](#domain-experts), feel free to follow the [Reviewer roulette](#reviewer-roulette) suggestion. + however, if one isn't available or you think the merge request doesn't need a review by a [domain expert](#domain-experts), feel free to follow the [Reviewer roulette](#reviewer-roulette) suggestion. - Remove yourself as a reviewer. ### The responsibility of the maintainer @@ -580,7 +580,7 @@ experience, refactors the existing code). Then: optionally resolve within the merge request or follow-up at a later stage. - There's a [Chrome/Firefox add-on](https://gitlab.com/conventionalcomments/conventional-comments-button) which you can use to apply [Conventional Comment](https://conventionalcomments.org/) prefixes. - Ensure there are no open dependencies. Check [linked issues](../user/project/issues/related_issues.md) for blockers. Clarify with the authors -if necessary. If blocked by one or more open MRs, set an [MR dependency](../user/project/merge_requests/dependencies.md). + if necessary. If blocked by one or more open MRs, set an [MR dependency](../user/project/merge_requests/dependencies.md). - After a round of line notes, it can be helpful to post a summary note such as "Looks good to me", or "Just a couple things to address." - Let the author know if changes are required following your review. diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md index a0888006937..c6027e27310 100644 --- a/doc/development/contributing/design.md +++ b/doc/development/contributing/design.md @@ -19,12 +19,12 @@ with additions and improvements. As a merge request (MR) author, you must: - Include _Before_ and _After_ -screenshots (or videos) of your changes in the description, as explained in our -[MR workflow](merge_request_workflow.md). These screenshots/videos are very helpful -for all reviewers and can speed up the review process, especially if the changes -are small. + screenshots (or videos) of your changes in the description, as explained in our + [MR workflow](merge_request_workflow.md). These screenshots/videos are very helpful + for all reviewers and can speed up the review process, especially if the changes + are small. - Attach the ~UX label to any merge request that has any user facing changes. This will trigger our -Reviewer Roulette to suggest a UX [reviewer](https://about.gitlab.com/handbook/product/ux/product-designer/mr-reviews/#stage-group-mrs). + Reviewer Roulette to suggest a UX [reviewer](https://about.gitlab.com/handbook/product/ux/product-designer/mr-reviews/#stage-group-mrs). If you are a **team member**: We recommend assigning the Product Designer suggested by the [Reviewer Roulette](../code_review.md#reviewer-roulette) as reviewer. [This helps us](https://about.gitlab.com/handbook/product/ux/product-designer/mr-reviews/#benefits) spread work evenly, improve communication, and make our UI more diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md index f5edbec88e1..be10688766f 100644 --- a/doc/development/documentation/site_architecture/global_nav.md +++ b/doc/development/documentation/site_architecture/global_nav.md @@ -52,7 +52,7 @@ the consent of one of the technical writers. To add a topic to the global navigation: 1. In the [`navigation.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/blob/main/content/_data/navigation.yaml) -file, add the item. + file, add the item. 1. Assign the MR to a technical writer for review and merge. ### Where to add diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 03a4b3eec94..c7783157575 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1140,7 +1140,7 @@ When you take screenshots: Reduce the size of your browser window as much as possible to keep elements close together and reduce empty space. Try to keep the screenshot dimensions as small as possible. - **Review how the image renders on the page.** Preview the image locally or use the -review app in the merge request. Make sure the image isn't blurry or overwhelming. + review app in the merge request. Make sure the image isn't blurry or overwhelming. - **Be consistent.** Coordinate screenshots with the other screenshots already on a documentation page for a consistent reading experience. Ensure your navigation theme is **Indigo** and the syntax highlighting theme is **Light**. These are the default preferences. diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index dfc18cf4139..fed295b8ec9 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -1595,7 +1595,7 @@ Searching is different from [filtering](#filter). When referring to the subscription billing model: - For GitLab SaaS, use **seats**. Customers purchase seats. Users occupy seats when they are invited -to a group, with some [exceptions](../../../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined). + to a group, with some [exceptions](../../../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined). - For GitLab self-managed, use **users**. Customers purchase subscriptions for a specified number of **users**. ## section diff --git a/doc/development/documentation/topic_types/troubleshooting.md b/doc/development/documentation/topic_types/troubleshooting.md index aee5bd1377c..f970b58e4fc 100644 --- a/doc/development/documentation/topic_types/troubleshooting.md +++ b/doc/development/documentation/topic_types/troubleshooting.md @@ -64,7 +64,7 @@ The workaround is... If multiple causes or solutions exist, consider putting them into a table format. If you use the exact error message, surround it in backticks so it's styled as code. -For more guidance on solution types, see [workaround](../../documentation/styleguide/word_list.md#workaround) and [resolution, resolve](../../documentation/styleguide/word_list.md#resolution-resolve). +For more guidance on solution types, see [workaround](../../documentation/styleguide/word_list.md#workaround) and [resolution, resolve](../../documentation/styleguide/word_list.md#resolution-resolve). ## Troubleshooting topic titles diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 08045675295..78177612aa9 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -200,7 +200,7 @@ To guard your licensed feature: ``` 1. Optional. If your global feature is also available to namespaces with a paid plan, combine two -feature identifiers to allow both administrators and group users. For example: + feature identifiers to allow both administrators and group users. For example: ```ruby License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # Both admins and group members can see this EE feature diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index a9f4b22c778..c6471d9720c 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -26,7 +26,7 @@ can still have specifics. They are described in their respective The Go upgrade documentation [provides an overview](go_upgrade.md#overview) of how GitLab manages and ships Go binary support. -If a GitLab component requires a newer version of Go, +If a GitLab component requires a newer version of Go, follow the [upgrade process](go_upgrade.md#updating-go-version) to ensure no customer, team, or component is adversely impacted. Sometimes, individual projects must also [manage builds with multiple versions of Go](go_upgrade.md#supporting-multiple-go-versions). diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md index 7149d431c30..ed3afb8efa6 100644 --- a/doc/development/i18n/translation.md +++ b/doc/development/i18n/translation.md @@ -96,7 +96,7 @@ For example, in German, the word _user_ can be translated into _Benutzer_ (male) ### Updating the glossary -To propose additions to the glossary, +To propose additions to the glossary, [open an issue](https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&utf8=✓&state=all&label_name[]=Category%3AInternationalization). ## French translation guidelines diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 30f598ef736..be76680c44c 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -723,7 +723,7 @@ The `with_lock_retries` method **cannot** be used within the `change` method, yo 1. For each iteration, set a pre-configured `lock_timeout`. 1. Try to execute the given block. (`remove_column`). 1. If `LockWaitTimeout` error is raised, sleep for the pre-configured `sleep_time` -and retry the block. + and retry the block. 1. If no error is raised, the current iteration has successfully executed the block. For more information check the [`Gitlab::Database::WithLockRetries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/with_lock_retries.rb) class. The `with_lock_retries` helper method is implemented in the [`Gitlab::Database::MigrationHelpers`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration_helpers.rb) module. @@ -733,7 +733,7 @@ In a worst-case scenario, the method: - Executes the block for a maximum of 50 times over 40 minutes. - Most of the time is spent in a pre-configured sleep period after each iteration. - After the 50th retry, the block is executed without `lock_timeout`, just -like a standard migration invocation. + like a standard migration invocation. - If a lock cannot be acquired, the migration fails with `statement timeout` error. The migration might fail if there is a very long running transaction (40+ minutes) diff --git a/doc/development/sec/analyzer_development_guide.md b/doc/development/sec/analyzer_development_guide.md index eb59d8fcaf5..843f41300a2 100644 --- a/doc/development/sec/analyzer_development_guide.md +++ b/doc/development/sec/analyzer_development_guide.md @@ -128,7 +128,7 @@ To use Docker with `replace` in the `go.mod` file: 1. Copy the contents of `command` into the directory of the analyzer. `cp -r /path/to/command path/to/analyzer/command`. 1. Add a copy statement in the analyzer's `Dockerfile`: `COPY command /command`. 1. Update the `replace` statement to make sure it matches the destination of the `COPY` statement in the step above: -`replace gitlab.com/gitlab-org/security-products/analyzers/command/v3 => /command` + `replace gitlab.com/gitlab-org/security-products/analyzers/command/v3 => /command` ## Analyzer scripts @@ -189,11 +189,11 @@ are integrated with the existing application, iteration should not be blocked by 1. Ensure that the release source (typically the `master` or `main` branch) has a passing pipeline. 1. Create a new release for the analyzer project by selecting the **Deployments** menu on the left-hand side of the project window, then selecting the **Releases** sub-menu. 1. Select **New release** to open the **New Release** page. - 1. In the **Tag name** drop down, enter the same version used in the `CHANGELOG.md`, for example `v2.4.2`, and select the option to create the tag (`Create tag v2.4.2` here). - 1. In the **Release title** text box enter the same version used above, for example `v2.4.2`. - 1. In the `Release notes` text box, copy and paste the notes from the corresponding version in the `CHANGELOG.md`. - 1. Leave all other settings as the default values. - 1. Select **Create release**. + 1. In the **Tag name** drop down, enter the same version used in the `CHANGELOG.md`, for example `v2.4.2`, and select the option to create the tag (`Create tag v2.4.2` here). + 1. In the **Release title** text box enter the same version used above, for example `v2.4.2`. + 1. In the `Release notes` text box, copy and paste the notes from the corresponding version in the `CHANGELOG.md`. + 1. Leave all other settings as the default values. + 1. Select **Create release**. After following the above process and creating a new release, a new Git tag is created with the `Tag name` provided above. This triggers a new pipeline with the given tag version and a new analyzer Docker image is built. @@ -229,7 +229,7 @@ After the above steps have been completed, the automatic release process execute 1. After a new version of the analyzer Docker image has been tagged and deployed, test it with the corresponding test project. 1. Announce the release on the relevant group Slack channel. Example message: - > FYI I've just released `ANALYZER_NAME` `ANALYZER_VERSION`. `LINK_TO_RELEASE` + > FYI I've just released `ANALYZER_NAME` `ANALYZER_VERSION`. `LINK_TO_RELEASE` **Never delete a Git tag that has been pushed** as there is a good chance that the tag will be used and/or cached by the Go package registry. diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md index a575d1ff890..d8fad6deb9c 100644 --- a/doc/development/secure_coding_guidelines.md +++ b/doc/development/secure_coding_guidelines.md @@ -182,7 +182,7 @@ For other regular expressions, here are a few guidelines: - If there's a clean non-regex solution, such as `String#start_with?`, consider using it - Ruby supports some advanced regex features like [atomic groups](https://www.regular-expressions.info/atomic.html) -and [possessive quantifiers](https://www.regular-expressions.info/possessive.html) that eliminate backtracking + and [possessive quantifiers](https://www.regular-expressions.info/possessive.html) that eliminate backtracking - Avoid nested quantifiers if possible (for example `(a+)+`) - Try to be as precise as possible in your regex and avoid the `.` if there's an alternative - For example, Use `_[^_]+_` instead of `_.*_` to match `_text here_` diff --git a/doc/development/testing_guide/end_to_end/feature_flags.md b/doc/development/testing_guide/end_to_end/feature_flags.md index e11119d2c0b..189b21ea607 100644 --- a/doc/development/testing_guide/end_to_end/feature_flags.md +++ b/doc/development/testing_guide/end_to_end/feature_flags.md @@ -24,14 +24,14 @@ Be sure to include the `feature_flag` tag so that the test can be skipped on the - Format: `feature_flag: { name: 'feature_flag_name' }` - Used only for informational purposes at this time. It should be included to help quickly determine what -feature flag is under test. + feature flag is under test. `scope` - Format: `feature_flag: { name: 'feature_flag_name', scope: :project }` - When `scope` is set to `:global`, the test will be **skipped on all live .com environments**. This is to avoid issues with feature flag changes affecting other tests or users on that environment. - When `scope` is set to any other value (such as `:project`, `:group` or `:user`), or if no `scope` is specified, the test will only be **skipped on canary, production, and pre-production**. -This is due to the fact that administrator access is not available there. + This is due to the fact that administrator access is not available there. **WARNING:** You are strongly advised to first try and [enable feature flags only for a group, project, user](../../feature_flags/index.md#feature-actors), or [feature group](../../feature_flags/index.md#feature-groups). diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index 1895b9bdb39..4d6fac57c06 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -119,7 +119,7 @@ Adding a delay in API or controller could help reproducing the issue. - [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101728/diffs): A CSS selector only appears after a GraphQL requests has finished, and the UI has updated. - [Example 3](https://gitlab.com/gitlab-org/gitlab/-/issues/408215): A false-positive test, Capybara immediately returns true after -page visit and page is not fully loaded, or if the element is not detectable by webdriver (such as being rendered outside the viewport or behind other elements). + page visit and page is not fully loaded, or if the element is not detectable by webdriver (such as being rendered outside the viewport or behind other elements). ### Datetime-sensitive diff --git a/doc/install/cloud_providers.md b/doc/install/cloud_providers.md index 318895b6d89..407c2152569 100644 --- a/doc/install/cloud_providers.md +++ b/doc/install/cloud_providers.md @@ -1,8 +1,8 @@ --- stage: Systems group: Distribution +description: AWS, Google Cloud Platform, Azure. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments -description: Install GitLab on a cloud provider. --- # Installing GitLab on a cloud provider **(FREE SELF)** diff --git a/doc/install/install_methods.md b/doc/install/install_methods.md index b40d89e957a..99be2709564 100644 --- a/doc/install/install_methods.md +++ b/doc/install/install_methods.md @@ -1,8 +1,8 @@ --- stage: Systems group: Distribution +description: Linux, Helm, Docker, Operator, source, or scripts. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments -description: Read through the GitLab installation methods. --- # Installation methods **(FREE SELF)** diff --git a/doc/install/requirements.md b/doc/install/requirements.md index fa6be957d99..e3905dd4882 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -1,6 +1,7 @@ --- stage: Systems group: Distribution +description: Prerequisites for installation. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/security/index.md b/doc/security/index.md index 01f227efbf9..ffc436e4286 100644 --- a/doc/security/index.md +++ b/doc/security/index.md @@ -1,6 +1,7 @@ --- stage: Govern group: Authentication +description: SSH key limits, 2FA, tokens, hardening. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md index 76b6c59c9fe..74ecf1701a4 100644 --- a/doc/subscriptions/gitlab_com/index.md +++ b/doc/subscriptions/gitlab_com/index.md @@ -232,13 +232,13 @@ After you dismiss the alert, it doesn't display until another seat is used. The alert displays based on the following seat usage. You cannot configure the amounts at which the alert displays. -| Seats in subscription | Seat usage | -|-----------------------|------------------------------------------------------------------------| -| 0-15 | One seat remaining in the subscription. | -| 16-25 | Two seats remaining in the subscription. | -| 26-99 | 10% of seats have been used. | -| 100-999 | 8% of seats have been used. | -| 1000+ | 5% of seats have been used | +| Seats in subscription | Seat usage | +|-----------------------|------------| +| 0-15 | One seat remaining in the subscription. | +| 16-25 | Two seats remaining in the subscription. | +| 26-99 | 10% of seats have been used. | +| 100-999 | 8% of seats have been used. | +| 1000+ | 5% of seats have been used | ## Change the linked namespace @@ -324,8 +324,8 @@ You can only renew your subscription 15 days before it is due to expire. To renew your subscription: 1. Sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and beneath your existing subscription, select **Renew**. -The **Renew** button displays only 15 days before a subscription expires. If there are more than 15 days before -the subscription expires, select **Subscription actions** (**{ellipsis_v}**), then select **Renew subscription** to view the date when you can renew. + The **Renew** button displays only 15 days before a subscription expires. If there are more than 15 days before + the subscription expires, select **Subscription actions** (**{ellipsis_v}**), then select **Renew subscription** to view the date when you can renew. 1. Review your renewal details and complete the payment process. 1. Select **Confirm purchase**. diff --git a/doc/topics/offline/index.md b/doc/topics/offline/index.md index b29a06463ba..b463abc79d3 100644 --- a/doc/topics/offline/index.md +++ b/doc/topics/offline/index.md @@ -1,6 +1,7 @@ --- stage: Systems group: Distribution +description: Isolated installation. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md index 3bf01e9143e..8efa056e70d 100644 --- a/doc/update/versions/gitlab_16_changes.md +++ b/doc/update/versions/gitlab_16_changes.md @@ -81,7 +81,7 @@ Specific information applies to Linux package installations: - PostgreSQL version 14 is the default for fresh installations of GitLab 16.7 and later. However, due to an [issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7768#note_1652076255), existing Geo secondary sites cannot be upgraded to PostgreSQL version 14. All Geo sites must run the same version of PostgreSQL. If you are adding a new Geo secondary site based on GitLab 16.7 you must take one of the following actions based on your configuration: - You are adding your first Geo secondary site: [Upgrade the Primary site to PostgreSQL 14](https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server) before setting up the new Geo secondary site. No special action is required if your primary site is already running PostgreSQL 14. - - You are adding a new Geo secondary site to a deployment that already has one or more Geo secondaries: + - You are adding a new Geo secondary site to a deployment that already has one or more Geo secondaries: - All sites are running PostgreSQL 13: Install the new Geo secondary site with [pinned PostgreSQL version 13](https://docs.gitlab.com/omnibus/settings/database.html#pin-the-packaged-postgresql-version-fresh-installs-only). - All sites are running PostgreSQL 14: No special action is required. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index a3f12d157f9..d8eab5f9da8 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -128,10 +128,6 @@ Setting `CS_DEFAULT_BRANCH_IMAGE` avoids duplicate vulnerability findings when a The value of `CS_DEFAULT_BRANCH_IMAGE` indicates the name of the scanned image as it appears on the default branch. For more details on how this deduplication is achieved, see [Setting the default branch image](#setting-the-default-branch-image). -## Running jobs in merge request pipelines - -See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines) - ### Customizing the container scanning settings There may be cases where you want to customize how GitLab scans your containers. For example, you @@ -243,6 +239,10 @@ if [Dependency Scanning](../dependency_scanning/index.md) is enabled for your project. This happens because GitLab can't automatically deduplicate findings across different types of scanning tools. To understand which types of dependencies are likely to be duplicated, see [Dependency Scanning compared to Container Scanning](../comparison_dependency_and_container_scanning.md). +#### Running jobs in merge request pipelines + +See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines). + #### Available CI/CD variables You can [configure](#customizing-the-container-scanning-settings) analyzers by using the following CI/CD variables. diff --git a/doc/user/application_security/dependency_scanning/troubleshooting_dependency_scanning.md b/doc/user/application_security/dependency_scanning/troubleshooting_dependency_scanning.md index dae72e1a555..83004459051 100644 --- a/doc/user/application_security/dependency_scanning/troubleshooting_dependency_scanning.md +++ b/doc/user/application_security/dependency_scanning/troubleshooting_dependency_scanning.md @@ -125,7 +125,7 @@ The lock file is cached during the build phase and passed to the dependency scan scan occurs. Because the cache is downloaded before the analyzer run occurs, the existence of a lock file in the `CI_BUILDS_DIR` directory triggers the dependency scanning job. -To prevent this warning, lock files should be committed. +To prevent this warning, lock files should be committed. ## You no longer get the latest Docker image after setting `DS_MAJOR_VERSION` or `DS_ANALYZER_IMAGE` diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index f309b0f11fb..b626bdef929 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -318,10 +318,6 @@ When downloading, you always receive the most recent SAST artifact available. You can enable and configure SAST by using the UI, either with the default settings or with customizations. The method you can use depends on your GitLab license tier. -### Running jobs in merge request pipelines - -See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines) - #### Configure SAST with customizations **(ULTIMATE ALL)** > [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/410013) individual SAST analyzers configuration options from the UI in GitLab 16.2. @@ -520,6 +516,10 @@ spotbugs-sast: sast: gl-sast-report.json ``` +### Running jobs in merge request pipelines + +See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines). + ### Available CI/CD variables SAST can be configured using the [`variables`](../../../ci/yaml/index.md#variables) parameter in diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md index 0eb79bfbe5a..9e2d67237d3 100644 --- a/doc/user/application_security/secret_detection/index.md +++ b/doc/user/application_security/secret_detection/index.md @@ -110,10 +110,6 @@ Secret Detection can detect if a secret was added in one commit and removed in a [merge request pipelines](../../../ci/pipelines/merge_request_pipelines.md). Secret Detection's results are only available after the pipeline is completed. -## Running jobs in merge request pipelines - -See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines) - ## Enable Secret Detection Prerequisites: @@ -265,6 +261,10 @@ For example: "A personal token for GitLab will look like glpat-JUST20LETTERSANDNUMB" #gitleaks:allow ``` +### Running jobs in merge request pipelines + +See [Use security scanning tools with merge request pipelines](../index.md#use-security-scanning-tools-with-merge-request-pipelines). + ### Available CI/CD variables Secret Detection can be customized by defining available CI/CD variables: diff --git a/doc/user/feature_flags.md b/doc/user/feature_flags.md index 83e2926e8e3..ccce9e9f9b4 100644 --- a/doc/user/feature_flags.md +++ b/doc/user/feature_flags.md @@ -1,8 +1,8 @@ --- stage: none group: unassigned -info: "See the Technical Writers assigned to Development Guidelines: https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines" -description: "View a list of all the flags available in the GitLab application." +description: Complete list of flags. +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments layout: 'feature_flags' --- diff --git a/doc/user/organization/index.md b/doc/user/organization/index.md index c4fff4178f1..ecc62b0d510 100644 --- a/doc/user/organization/index.md +++ b/doc/user/organization/index.md @@ -44,6 +44,8 @@ To view the organizations you have access to: 1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**. 1. In the **Organization name** text box, enter a name for the organization. 1. In the **Organization URL** text box, enter a path for the organization. +1. In the **Organization description** text box, enter a description for the organization. Supports a [limited subset of Markdown](#supported-markdown-for-organization-description). +1. In the **Organization avatar** field, select **Upload** or drag and drop an avatar. 1. Select **Create organization**. ## Edit an organization's name @@ -51,6 +53,10 @@ To view the organizations you have access to: 1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit. 1. Select **Settings > General**. 1. In the **Organization name** text box, edit the name. +1. In the **Organization description** text box, edit the description. Supports a [limited subset of Markdown](#supported-markdown-for-organization-description). +1. In the **Organization avatar** field, if an avatar is: + - Selected, select **Remove avatar** to remove. + - Not selected, select **Upload** or drag and drop an avatar. 1. Select **Save changes**. ## Change an organization's URL @@ -72,6 +78,14 @@ To view the organizations you have access to: 1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage. 1. Select **Manage > Users**. +## Supported Markdown for Organization description + +The Organization description field supports a limited subset of [GitLab Flavored Markdown](../markdown.md), including: + +- [Emphasis](../markdown.md#emphasis) +- [Links](../markdown.md#links) +- [Superscripts / Subscripts](../markdown.md#superscripts--subscripts) + ## Related topics - [Organization developer documentation](../../development/organization/index.md) diff --git a/doc/user/profile/account/create_accounts.md b/doc/user/profile/account/create_accounts.md index 6c78152fa70..4611b3a6a40 100644 --- a/doc/user/profile/account/create_accounts.md +++ b/doc/user/profile/account/create_accounts.md @@ -1,6 +1,7 @@ --- stage: Govern group: Authentication +description: Passwords, user moderation, broadcast messages. info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/repository/code_suggestions/self_managed.md b/doc/user/project/repository/code_suggestions/self_managed.md index 5688985ab49..8cd499c13d0 100644 --- a/doc/user/project/repository/code_suggestions/self_managed.md +++ b/doc/user/project/repository/code_suggestions/self_managed.md @@ -139,7 +139,7 @@ In GitLab 16.3 and later, GitLab is enforcing the cloud licensing requirement fo If you have a GitLab Free subscription and upgrade to GitLab 16.3 or later, to continue having early access to Code Suggestions, you must: -1. Have a [subscription that supports cloud licensing](https://about.gitlab.com/pricing/). +1. Have a [subscription that supports cloud licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/). 1. Make sure you have the latest version of your [IDE extension](index.md#supported-editor-extensions). 1. [Manually synchronize your subscription](#manually-synchronize-your-subscription). diff --git a/gems/gitlab-database-load_balancing/Gemfile.lock b/gems/gitlab-database-load_balancing/Gemfile.lock index a6148494c1b..953b84ea84f 100644 --- a/gems/gitlab-database-load_balancing/Gemfile.lock +++ b/gems/gitlab-database-load_balancing/Gemfile.lock @@ -16,6 +16,7 @@ PATH remote: ../gitlab-safe_request_store specs: gitlab-safe_request_store (0.1.0) + rack (~> 2.2.8) request_store PATH diff --git a/gems/gitlab-safe_request_store/Gemfile.lock b/gems/gitlab-safe_request_store/Gemfile.lock index 50ff694d7d5..d26bab2f4f0 100644 --- a/gems/gitlab-safe_request_store/Gemfile.lock +++ b/gems/gitlab-safe_request_store/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: gitlab-safe_request_store (0.1.0) + rack (~> 2.2.8) request_store GEM @@ -35,7 +36,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) racc (1.6.2) - rack (3.0.4.1) + rack (2.2.8) rainbow (3.1.1) regexp_parser (2.7.0) request_store (1.5.1) diff --git a/gems/gitlab-safe_request_store/gitlab-safe_request_store.gemspec b/gems/gitlab-safe_request_store/gitlab-safe_request_store.gemspec index a685a1eb447..b3ced3929c8 100644 --- a/gems/gitlab-safe_request_store/gitlab-safe_request_store.gemspec +++ b/gems/gitlab-safe_request_store/gitlab-safe_request_store.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |spec| spec.files = Dir['lib/**/*.rb'] spec.require_paths = ["lib"] + spec.add_runtime_dependency "rack", "~> 2.2.8" spec.add_runtime_dependency "request_store" spec.add_development_dependency "gitlab-styles", "~> 10.1.0" diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 17bee275c51..300c30faf4a 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -94,6 +94,14 @@ module API forbidden!("No access granted") unless can?(current_user, :read_builds, runner) end + + def preload_job_associations(jobs) + jobs.preload( # rubocop: disable CodeReuse/ActiveRecord -- this preload is tightly related to the endpoint + :user, + { pipeline: { project: [:route, { namespace: :route }] } }, + { project: [:route, { namespace: :route }] } + ) + end end resource :runners do @@ -217,25 +225,24 @@ module API end params do requires :id, type: Integer, desc: 'The ID of a runner' + optional :system_id, type: String, desc: 'System ID associated with the runner manager' optional :status, type: String, desc: 'Status of the job', values: ::Ci::Build::AVAILABLE_STATUSES optional :order_by, type: String, desc: 'Order by `id`', values: ::Ci::RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by `asc` or `desc` order. ' \ - 'Specify `order_by` as well, including for `id`' + 'Specify `order_by` as well, including for `id`' + optional :cursor, type: String, desc: 'Cursor for obtaining the next set of records' use :pagination end get ':id/jobs' do runner = get_runner(params[:id]) authenticate_list_runners_jobs!(runner) + # Optimize query when filtering by runner managers by not asking for count + paginator_params = params[:pagination] == :keyset || params[:system_id].blank? ? {} : { without_count: true } + jobs = ::Ci::RunnerJobsFinder.new(runner, current_user, params).execute - jobs = jobs.preload( # rubocop: disable CodeReuse/ActiveRecord - [ - :user, - { pipeline: { project: [:route, { namespace: :route }] } }, - { project: [:route, { namespace: :route }] } - ] - ) - jobs = paginate(jobs) + jobs = preload_job_associations(jobs) + jobs = paginate_with_strategies(jobs, paginator_params: paginator_params) jobs.each(&:commit) # batch loads all commits in the page present jobs, with: Entities::Ci::JobBasicWithProject diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 74bec55253f..2c9b559c8dc 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -31,13 +31,15 @@ module Gitlab end def external_url(name, external_ref) - return if GIT_INVALID_URL_REGEX.match?(external_ref) + ref = external_ref.to_s - case external_ref + return if GIT_INVALID_URL_REGEX.match?(ref) + + case ref when /\A#{URL_REGEX}\z/o - external_ref + ref when /\A#{REPO_REGEX}\z/o - github_url(external_ref) + github_url(ref) else package_url(name) end diff --git a/lib/gitlab/middleware/unauthenticated_session_expiry.rb b/lib/gitlab/middleware/unauthenticated_session_expiry.rb index f240a6b23bd..7c5c523c287 100644 --- a/lib/gitlab/middleware/unauthenticated_session_expiry.rb +++ b/lib/gitlab/middleware/unauthenticated_session_expiry.rb @@ -18,8 +18,9 @@ module Gitlab result = @app.call(env) warden = env['warden'] + user = catch(:warden) { warden && warden.user } # rubocop:disable Cop/BanCatchThrow -- ignore Warden errors since we're outside Warden::Manager - unless warden && warden.user + unless user # This works because Rack uses these options every time a request is handled, and redis-store # uses the Rack setting first: # 1. https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L342 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 21862bdba69..920fbe3ad06 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34035,6 +34035,9 @@ msgstr "" msgid "Organization|Organization avatar" msgstr "" +msgid "Organization|Organization description (optional)" +msgstr "" + msgid "Organization|Organization name" msgstr "" @@ -35618,12 +35621,30 @@ msgstr "" msgid "PipelineStatusTooltip|Pipeline: %{ciStatus}" msgstr "" +msgid "PipelineSubscriptions|Add new" +msgstr "" + msgid "PipelineSubscriptions|An error occurred while fetching downstream pipeline subscriptions." msgstr "" msgid "PipelineSubscriptions|An error occurred while fetching upstream pipeline subscriptions." msgstr "" +msgid "PipelineSubscriptions|Delete subscription" +msgstr "" + +msgid "PipelineSubscriptions|No project subscribes to the pipelines in this project." +msgstr "" + +msgid "PipelineSubscriptions|Subscribed to this project" +msgstr "" + +msgid "PipelineSubscriptions|Subscriptions" +msgstr "" + +msgid "PipelineSubscriptions|This project is not subscribed to any project pipelines." +msgstr "" + msgid "PipelineWizardDefaultCommitMessage|Add %{filename}" msgstr "" diff --git a/rubocop/cop/scalability/file_uploads.rb b/rubocop/cop/scalability/file_uploads.rb index fc52444c551..8f8c9605d09 100644 --- a/rubocop/cop/scalability/file_uploads.rb +++ b/rubocop/cop/scalability/file_uploads.rb @@ -27,7 +27,7 @@ module RuboCop # class FileUploads < RuboCop::Cop::Base MSG = 'Do not upload files without workhorse acceleration. ' \ - 'Please refer to https://docs.gitlab.com/ee/development/uploads.html' + 'Please refer to https://docs.gitlab.com/ee/development/uploads/' def_node_matcher :file_in_type, <<~PATTERN (send nil? {:requires :optional} diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index a2848bd0256..83107e6cc4a 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -2,6 +2,10 @@ require_relative '../support/helpers/test_env' +# TODO: Remove the debug_with_puts statements below! Used for debugging purposes. +# TODO: https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/323#note_1688925316 +require_relative '../support/helpers/debug_with_puts' + FactoryBot.define do # Project without repository # @@ -66,6 +70,8 @@ FactoryBot.define do end after(:build) do |project, evaluator| + DebugWithPuts.debug_with_puts "Beginning of after :build of projects factory in spec/factories/projects.rb" + # Builds and MRs can't have higher visibility level than repository access level. builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min @@ -87,6 +93,8 @@ FactoryBot.define do security_and_compliance_access_level: evaluator.security_and_compliance_access_level } + DebugWithPuts.debug_with_puts "During after :build of projects factory in spec/factories/projects.rb:#{__LINE__}" + project_namespace_hash = { name: evaluator.name, path: evaluator.path, @@ -97,10 +105,16 @@ FactoryBot.define do project_namespace_hash[:id] = evaluator.project_namespace_id.presence + DebugWithPuts.debug_with_puts "During after :build of projects factory in spec/factories/projects.rb:#{__LINE__}" + project.build_project_namespace(project_namespace_hash) project.build_project_feature(project_feature_hash) + DebugWithPuts.debug_with_puts "During after :build of projects factory in spec/factories/projects.rb:#{__LINE__}" + project.set_runners_token(evaluator.runners_token) if evaluator.runners_token.present? + + DebugWithPuts.debug_with_puts "End of after :build of projects factory in spec/factories/projects.rb" end to_create do |project| @@ -108,6 +122,7 @@ FactoryBot.define do end after(:create) do |project, evaluator| + DebugWithPuts.debug_with_puts "Beginning of after :create of projects factory in spec/factories/projects.rb" # Normally the class Projects::CreateService is used for creating # projects, and this class takes care of making sure the owner and current # user have access to the project. Our specs don't use said service class, @@ -116,12 +131,16 @@ FactoryBot.define do project.add_owner(project.first_owner) end + DebugWithPuts.debug_with_puts "During after :create of projects factory in spec/factories/projects.rb:#{__LINE__}" + if project.group project.run_after_commit_or_now do AuthorizedProjectUpdate::ProjectRecalculateService.new(project).execute end end + DebugWithPuts.debug_with_puts "During after :create of projects factory in spec/factories/projects.rb:#{__LINE__}" + # assign the delegated `#ci_cd_settings` attributes after create project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil? project.merge_pipelines_enabled = evaluator.merge_pipelines_enabled unless evaluator.merge_pipelines_enabled.nil? @@ -133,6 +152,8 @@ FactoryBot.define do project.runner_token_expiration_interval = evaluator.runner_token_expiration_interval unless evaluator.runner_token_expiration_interval.nil? project.runner_token_expiration_interval_human_readable = evaluator.runner_token_expiration_interval_human_readable unless evaluator.runner_token_expiration_interval_human_readable.nil? + DebugWithPuts.debug_with_puts "During after :create of projects factory in spec/factories/projects.rb:#{__LINE__}" + if evaluator.import_status import_state = project.import_state || project.build_import_state import_state.status = evaluator.import_status @@ -142,8 +163,12 @@ FactoryBot.define do import_state.save! end + DebugWithPuts.debug_with_puts "During after :create of projects factory in spec/factories/projects.rb:#{__LINE__}" + # simulating ::Projects::ProcessSyncEventsWorker because most tests don't run Sidekiq inline project.create_ci_project_mirror!(namespace_id: project.namespace_id) unless project.ci_project_mirror + + DebugWithPuts.debug_with_puts "End of after :create of projects factory in spec/factories/projects.rb" end trait :public do @@ -326,6 +351,7 @@ FactoryBot.define do end after :create do |project, evaluator| + DebugWithPuts.debug_with_puts "Beginning of after :create of trait :repository do in spec/factories/projects.rb" # Specify `lfs: true` to create the LfsObject for the LFS file in the test repo: # https://gitlab.com/gitlab-org/gitlab-test/-/blob/master/files/lfs/lfs_object.iso if evaluator.lfs @@ -351,6 +377,8 @@ FactoryBot.define do end end + DebugWithPuts.debug_with_puts "During after :create of trait :repository do in spec/factories/projects.rb:#{__LINE__}" + if evaluator.create_templates templates_path = "#{evaluator.create_templates}_templates" @@ -380,6 +408,8 @@ FactoryBot.define do branch_name: 'master') end + DebugWithPuts.debug_with_puts "During after :create of trait :repository do in spec/factories/projects.rb:#{__LINE__}" + if evaluator.create_branch project.repository.create_file( project.creator, @@ -389,6 +419,8 @@ FactoryBot.define do branch_name: evaluator.create_branch) end + DebugWithPuts.debug_with_puts "During after :create of trait :repository do in spec/factories/projects.rb:#{__LINE__}" + if evaluator.create_tag project.repository.add_tag( project.creator, @@ -397,6 +429,7 @@ FactoryBot.define do end project.track_project_repository + DebugWithPuts.debug_with_puts "End of after :create of trait :repository do in spec/factories/projects.rb" end end diff --git a/spec/finders/ci/runner_jobs_finder_spec.rb b/spec/finders/ci/runner_jobs_finder_spec.rb index 755b21ec08f..66cdde756be 100644 --- a/spec/finders/ci/runner_jobs_finder_spec.rb +++ b/spec/finders/ci/runner_jobs_finder_spec.rb @@ -2,26 +2,28 @@ require 'spec_helper' -RSpec.describe Ci::RunnerJobsFinder do - let(:project) { create(:project) } - let(:runner) { create(:ci_runner, :instance) } - let(:user) { create(:user) } +RSpec.describe Ci::RunnerJobsFinder, feature_category: :fleet_visibility do + let_it_be(:project) { create(:project) } + let_it_be(:runner) { create(:ci_runner, :instance) } + let_it_be(:user) { create(:user) } + let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) } + let_it_be(:jobs) { create_list(:ci_build, 5, runner_manager: runner_manager, project: project) } + let(:params) { {} } - subject { described_class.new(runner, user, params).execute } + subject(:returned_jobs) { described_class.new(runner, user, params).execute } - before do + before_all do project.add_developer(user) end describe '#execute' do context 'when params is empty' do - let!(:job) { create(:ci_build, runner: runner, project: project) } let!(:job1) { create(:ci_build, project: project) } it 'returns all jobs assigned to Runner' do - is_expected.to match_array(job) - is_expected.not_to match_array(job1) + is_expected.to match_array(jobs) + is_expected.not_to include(job1) end end @@ -36,35 +38,34 @@ RSpec.describe Ci::RunnerJobsFinder do end end - context 'when the user has permission to read all resources' do - let(:user) { create(:user, :admin) } + context 'when the user is admin', :enable_admin_mode do + let_it_be(:user) { create(:user, :admin) } - it 'returns all the jobs assigned to a runner' do - jobs = create_list(:ci_build, 5, runner: runner, project: project) + it { is_expected.to match_array(jobs) } + end - is_expected.to match_array(jobs) + context 'when user is developer' do + before_all do + project.add_developer(user) end + + it { is_expected.to match_array(jobs) } end context 'when the user has different access levels in different projects' do - it 'returns only the jobs the user has permission to see' do - guest_project = create(:project) - reporter_project = create(:project) - - _guest_jobs = create_list(:ci_build, 2, runner: runner, project: guest_project) - reporter_jobs = create_list(:ci_build, 3, runner: runner, project: reporter_project) - - guest_project.add_guest(user) - reporter_project.add_reporter(user) - - is_expected.to match_array(reporter_jobs) + let_it_be(:guest_project) { create(:project).tap { |p| p.add_guest(user) } } + let_it_be(:guest_jobs) { create_list(:ci_build, 2, runner: runner, project: guest_project) } + let_it_be(:reporter_project) { create(:project).tap { |p| p.add_reporter(user) } } + let_it_be(:reporter_jobs) { create_list(:ci_build, 3, runner: runner, project: reporter_project) } + + it 'returns only the jobs the user has permission to see', :aggregate_failures do + is_expected.to include(*reporter_jobs) + is_expected.not_to include(*guest_jobs) end end context 'when the user has reporter access level or greater' do - it 'returns jobs assigned to the Runner that the user has accesss to' do - jobs = create_list(:ci_build, 3, runner: runner, project: project) - + it 'returns jobs assigned to the Runner that the user has access to' do is_expected.to match_array(jobs) end end @@ -73,24 +74,38 @@ RSpec.describe Ci::RunnerJobsFinder do Ci::HasStatus::AVAILABLE_STATUSES.each do |target_status| context "when status is #{target_status}" do let(:params) { { status: target_status } } + let(:exception_status) { (Ci::HasStatus::AVAILABLE_STATUSES - [target_status]).first } let!(:job) { create(:ci_build, runner: runner, project: project, status: target_status) } + let!(:other_job) { create(:ci_build, runner: runner, project: project, status: exception_status) } - before do - exception_status = Ci::HasStatus::AVAILABLE_STATUSES - [target_status] - create(:ci_build, runner: runner, project: project, status: exception_status.first) - end - - it 'returns matched job' do - is_expected.to eq([job]) + it 'returns matched job', :aggregate_failures do + is_expected.to include(job) + is_expected.not_to include(other_job) end end end end + context 'when system_id is specified' do + let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner) } + let_it_be(:job2) { create(:ci_build, runner_manager: runner_manager2, project: project) } + + let(:params) { { system_id: runner_manager.system_xid } } + + it 'returns jobs from the specified system' do + expect(returned_jobs).to match_array(jobs) + end + + context 'when specified system_id does not exist' do + let(:params) { { system_id: 'unknown_system' } } + + it { is_expected.to be_empty } + end + end + context 'when order_by and sort are specified' do context 'when order_by id and sort is asc' do let(:params) { { order_by: 'id', sort: 'asc' } } - let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } it 'sorts as id: :asc' do is_expected.to eq(jobs.sort_by(&:id)) @@ -101,7 +116,6 @@ RSpec.describe Ci::RunnerJobsFinder do context 'when order_by is specified and sort is not specified' do context 'when order_by id and sort is not specified' do let(:params) { { order_by: 'id' } } - let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } it 'sorts as id: :desc' do is_expected.to eq(jobs.sort_by(&:id).reverse) diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js index 4f31baedbf6..e3e1c5b9684 100644 --- a/spec/frontend/organizations/new/components/app_spec.js +++ b/spec/frontend/organizations/new/components/app_spec.js @@ -24,10 +24,14 @@ describe('OrganizationNewApp', () => { let wrapper; let mockApollo; + const file = new File(['foo'], 'foo.jpg', { + type: 'text/plain', + }); + + const successfulResponseHandler = jest.fn().mockResolvedValue(organizationCreateResponse); + const createComponent = ({ - handlers = [ - [organizationCreateMutation, jest.fn().mockResolvedValue(organizationCreateResponse)], - ], + handlers = [[organizationCreateMutation, successfulResponseHandler]], } = {}) => { mockApollo = createMockApollo(handlers); @@ -36,7 +40,12 @@ describe('OrganizationNewApp', () => { const findForm = () => wrapper.findComponent(NewEditForm); const submitForm = async () => { - findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' }); + findForm().vm.$emit('submit', { + name: 'Foo bar', + path: 'foo-bar', + description: 'Foo bar description', + avatar: file, + }); await nextTick(); }; @@ -74,7 +83,15 @@ describe('OrganizationNewApp', () => { await waitForPromises(); }); - it('redirects user to organization web url', () => { + it('calls mutation with correct variables and redirects user to organization web url', () => { + expect(successfulResponseHandler).toHaveBeenCalledWith({ + input: { + name: 'Foo bar', + path: 'foo-bar', + description: 'Foo bar description', + avatar: file, + }, + }); expect(visitUrlWithAlerts).toHaveBeenCalledWith( organizationCreateResponse.data.organizationCreate.organization.webUrl, [ diff --git a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js index eca6d9fdc4a..52e81d7fb5d 100644 --- a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js +++ b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js @@ -9,6 +9,7 @@ import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR, + FORM_FIELD_DESCRIPTION, } from '~/organizations/shared/constants'; import organizationUpdateMutation from '~/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql'; import { @@ -62,7 +63,13 @@ describe('OrganizationSettings', () => { const findForm = () => wrapper.findComponent(NewEditForm); const submitForm = async (data = {}) => { - findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar', avatar: file, ...data }); + findForm().vm.$emit('submit', { + name: 'Foo bar', + path: 'foo-bar', + description: 'Foo bar description', + avatar: file, + ...data, + }); await nextTick(); }; @@ -84,7 +91,7 @@ describe('OrganizationSettings', () => { expect(findForm().props()).toMatchObject({ loading: false, initialFormValues: defaultProvide.organization, - fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR], + fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR], }); }); @@ -117,6 +124,7 @@ describe('OrganizationSettings', () => { input: { id: 'gid://gitlab/Organizations::Organization/1', name: 'Foo bar', + description: 'Foo bar description', avatar: file, }, }); @@ -191,6 +199,7 @@ describe('OrganizationSettings', () => { input: { id: 'gid://gitlab/Organizations::Organization/1', name: 'Foo bar', + description: 'Foo bar description', avatar: null, }, }); @@ -208,6 +217,7 @@ describe('OrganizationSettings', () => { input: { id: 'gid://gitlab/Organizations::Organization/1', name: 'Foo bar', + description: 'Foo bar description', }, }); }); diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js index 4897a81fc1c..5be26ef7cc3 100644 --- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js +++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js @@ -1,9 +1,10 @@ -import { GlButton } from '@gitlab/ui'; import { nextTick } from 'vue'; import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue'; import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { FORM_FIELD_NAME, FORM_FIELD_ID, @@ -18,6 +19,7 @@ describe('NewEditForm', () => { const defaultProvide = { organizationsPath: '/-/organizations', rootUrl: 'http://127.0.0.1:3000/', + previewMarkdownPath: '/-/organizations/preview_markdown', }; const defaultPropsData = { @@ -38,6 +40,7 @@ describe('NewEditForm', () => { const findNameField = () => wrapper.findByLabelText('Organization name'); const findIdField = () => wrapper.findByLabelText('Organization ID'); const findUrlField = () => wrapper.findComponent(OrganizationUrlField); + const findDescriptionField = () => wrapper.findByLabelText('Organization description (optional)'); const findAvatarField = () => wrapper.findComponent(AvatarUploadDropzone); const setUrlFieldValue = async (value) => { @@ -70,6 +73,30 @@ describe('NewEditForm', () => { }); }); + it('renders `Organization description` field as markdown editor', () => { + createComponent(); + + expect(findDescriptionField().exists()).toBe(true); + expect(wrapper.findComponent(MarkdownField).props()).toMatchObject({ + markdownPreviewPath: defaultProvide.previewMarkdownPath, + markdownDocsPath: helpPagePath('user/organization/index', { + anchor: 'organization-description-supported-markdown', + }), + textareaValue: '', + restrictedToolBarItems: [ + 'code', + 'quote', + 'bullet-list', + 'numbered-list', + 'task-list', + 'collapsible-section', + 'table', + 'attach-file', + 'full-screen', + ], + }); + }); + describe('when `Organization avatar` field is changed', () => { const file = new File(['foo'], 'foo.jpg', { type: 'text/plain', @@ -154,12 +181,13 @@ describe('NewEditForm', () => { await findNameField().setValue('Foo bar'); await setUrlFieldValue('foo-bar'); + await findDescriptionField().setValue('Foo bar description'); await submitForm(); }); it('emits `submit` event with form values', () => { expect(wrapper.emitted('submit')).toEqual([ - [{ name: 'Foo bar', path: 'foo-bar', avatar: null }], + [{ name: 'Foo bar', path: 'foo-bar', description: 'Foo bar description', avatar: null }], ]); }); }); @@ -221,7 +249,7 @@ describe('NewEditForm', () => { }); it('shows button with loading icon', () => { - expect(wrapper.findComponent(GlButton).props('loading')).toBe(true); + expect(wrapper.findByTestId('submit-button').props('loading')).toBe(true); }); }); diff --git a/spec/frontend/organizations/show/components/app_spec.js b/spec/frontend/organizations/show/components/app_spec.js index 46496e40bdd..6cf8845bdbe 100644 --- a/spec/frontend/organizations/show/components/app_spec.js +++ b/spec/frontend/organizations/show/components/app_spec.js @@ -1,6 +1,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import App from '~/organizations/show/components/app.vue'; import OrganizationAvatar from '~/organizations/show/components/organization_avatar.vue'; +import OrganizationDescription from '~/organizations/show/components/organization_description.vue'; import GroupsAndProjects from '~/organizations/show/components/groups_and_projects.vue'; import AssociationCount from '~/organizations/show/components/association_counts.vue'; @@ -34,6 +35,12 @@ describe('OrganizationShowApp', () => { ); }); + it('renders organization description and passes organization prop', () => { + expect(wrapper.findComponent(OrganizationDescription).props('organization')).toEqual( + defaultPropsData.organization, + ); + }); + it('renders groups and projects component and passes `groupsAndProjectsOrganizationPath` prop', () => { expect( wrapper.findComponent(GroupsAndProjects).props('groupsAndProjectsOrganizationPath'), diff --git a/spec/frontend/organizations/show/components/organization_description_spec.js b/spec/frontend/organizations/show/components/organization_description_spec.js new file mode 100644 index 00000000000..2aaf6f24a72 --- /dev/null +++ b/spec/frontend/organizations/show/components/organization_description_spec.js @@ -0,0 +1,46 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import OrganizationDescription from '~/organizations/show/components/organization_description.vue'; + +describe('OrganizationDescription', () => { + let wrapper; + + const defaultPropsData = { + organization: { + id: 1, + name: 'GitLab', + description_html: '<h1>Foo bar description</h1><script>alert("foo")</script>', + }, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(OrganizationDescription, { + propsData: { ...defaultPropsData, ...propsData }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('when organization has description', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders description as safe HTML', () => { + expect(wrapper.element.innerHTML).toBe('<h1>Foo bar description</h1>'); + }); + }); + + describe('when organization does not have description', () => { + beforeEach(() => { + createComponent({ + propsData: { organization: { ...defaultPropsData.organization, description_html: '' } }, + }); + }); + + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); + }); +}); diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index a3613e29da9..0f2f4ed1b54 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do - let_it_be(:organization) { build_stubbed(:organization) } + let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: '<em>description</em>') } + let_it_be(:organization) { organization_detail.organization } let_it_be(:new_group_path) { '/groups/new' } let_it_be(:new_project_path) { '/projects/new' } let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' } @@ -11,6 +12,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:root_url) { 'http://127.0.0.1:3000/' } let_it_be(:groups_empty_state_svg_path) { 'illustrations/empty-state/empty-groups-md.svg' } let_it_be(:projects_empty_state_svg_path) { 'illustrations/empty-state/empty-projects-md.svg' } + let_it_be(:preview_markdown_organizations_path) { '/-/organizations/preview_markdown' } before do allow(helper).to receive(:new_group_path).and_return(new_group_path) @@ -21,6 +23,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do allow(helper).to receive(:root_url).and_return(root_url) allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path) allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path) + allow(helper).to receive(:preview_markdown_organizations_path).and_return(preview_markdown_organizations_path) end describe '#organization_show_app_data' do @@ -41,6 +44,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do 'organization' => { 'id' => organization.id, 'name' => organization.name, + 'description_html' => organization.description_html, 'avatar_url' => 'avatar.jpg' }, 'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects', @@ -91,7 +95,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do expect(Gitlab::Json.parse(helper.organization_new_app_data)).to eq( { 'organizations_path' => organizations_path, - 'root_url' => root_url + 'root_url' => root_url, + 'preview_markdown_path' => preview_markdown_organizations_path } ) end @@ -119,10 +124,12 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do 'id' => organization.id, 'name' => organization.name, 'path' => organization.path, + 'description' => organization.description, 'avatar' => 'avatar.jpg' }, 'organizations_path' => organizations_path, - 'root_url' => root_url + 'root_url' => root_url, + 'preview_markdown_path' => preview_markdown_organizations_path } ) end diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb index 127f437dd54..e3cddceb7a9 100644 --- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -100,5 +100,21 @@ RSpec.describe Gitlab::DependencyLinker::PackageJsonLinker do it 'does not link scripts with the same key as a package' do expect(subject).not_to include(link('karma start config/karma.config.js --single-run', 'https://github.com/karma start config/karma.config.js --single-run')) end + + context 'when dependency is not a string' do + let(:file_content) do + <<-CONTENT.strip_heredoc + { + "dependencies": { + "wrong": {} + } + } + CONTENT + end + + it 'does not link it' do + expect(subject).not_to include(%(<a href)) + end + end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index af3f54c5f0a..c5c97228988 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -325,14 +325,15 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def describe '.with_exposed_artifacts' do subject { described_class.with_exposed_artifacts } - let!(:job1) { create(:ci_build, pipeline: pipeline) } + let_it_be(:job1) { create(:ci_build, pipeline: pipeline) } + let_it_be(:job3) { create(:ci_build, pipeline: pipeline) } + let!(:job2) { create(:ci_build, options: options, pipeline: pipeline) } - let!(:job3) { create(:ci_build, pipeline: pipeline) } - context 'when some jobs have exposed artifacs and some not' do + context 'when some jobs have exposed artifacts and some not' do let(:options) { { artifacts: { expose_as: 'test', paths: ['test'] } } } - before do + before_all do job1.ensure_metadata.update!(has_exposed_artifacts: nil) job3.ensure_metadata.update!(has_exposed_artifacts: false) end @@ -356,10 +357,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def let(:artifact_scope) { Ci::JobArtifact.where(file_type: 'archive') } - let!(:build_1) { create(:ci_build, :artifacts, pipeline: pipeline) } - let!(:build_2) { create(:ci_build, :codequality_reports, pipeline: pipeline) } - let!(:build_3) { create(:ci_build, :test_reports, pipeline: pipeline) } - let!(:build_4) { create(:ci_build, :artifacts, pipeline: pipeline) } + let_it_be(:build_1) { create(:ci_build, :artifacts, pipeline: pipeline) } + let_it_be(:build_2) { create(:ci_build, :codequality_reports, pipeline: pipeline) } + let_it_be(:build_3) { create(:ci_build, :test_reports, pipeline: pipeline) } + let_it_be(:build_4) { create(:ci_build, :artifacts, pipeline: pipeline) } it 'returns artifacts matching the given scope' do expect(builds).to contain_exactly(build_1, build_4) @@ -383,10 +384,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end describe '.with_needs' do - let!(:build) { create(:ci_build, pipeline: pipeline) } - let!(:build_b) { create(:ci_build, pipeline: pipeline) } - let!(:build_need_a) { create(:ci_build_need, build: build) } - let!(:build_need_b) { create(:ci_build_need, build: build_b) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be(:build_b) { create(:ci_build, pipeline: pipeline) } + let_it_be(:build_need_a) { create(:ci_build_need, build: build) } + let_it_be(:build_need_b) { create(:ci_build_need, build: build_b) } context 'when passing build name' do subject { described_class.with_needs(build_need_a.name) } @@ -421,6 +422,33 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end end + describe '.belonging_to_runner_manager' do + subject { described_class.belonging_to_runner_manager(runner_manager) } + + let_it_be(:runner) { create(:ci_runner, :group, groups: [group]) } + let_it_be(:build_b) { create(:ci_build, :success) } + + context 'with runner_manager of runner associated with build' do + let!(:runner_manager) { create(:ci_runner_machine, runner: runner) } + let!(:runner_manager_build) { create(:ci_runner_machine_build, build: build, runner_manager: runner_manager) } + + it { is_expected.to contain_exactly(build) } + end + + context 'with runner_manager of runner not associated with build' do + let!(:runner_manager) { create(:ci_runner_machine, runner: instance_runner) } + let!(:instance_runner) { create(:ci_runner, :with_runner_manager) } + + it { is_expected.to be_empty } + end + + context 'with nil runner_manager' do + let(:runner_manager) { nil } + + it { is_expected.to be_empty } + end + end + describe '#stick_build_if_status_changed' do it 'sticks the build if the status changed' do job = create(:ci_build, :pending, pipeline: pipeline) diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb index ca762f7d618..4fdfbae997d 100644 --- a/spec/models/ci/runner_manager_spec.rb +++ b/spec/models/ci/runner_manager_spec.rb @@ -122,23 +122,60 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo describe '.for_runner' do subject(:runner_managers) { described_class.for_runner(runner_arg) } - let_it_be(:runner1) { create(:ci_runner) } - let_it_be(:runner_manager11) { create(:ci_runner_machine, runner: runner1) } - let_it_be(:runner_manager12) { create(:ci_runner_machine, runner: runner1) } + let_it_be(:runner_a) { create(:ci_runner) } + let_it_be(:runner_manager_a1) { create(:ci_runner_machine, runner: runner_a) } + let_it_be(:runner_manager_a2) { create(:ci_runner_machine, runner: runner_a) } context 'with single runner' do - let(:runner_arg) { runner1 } + let(:runner_arg) { runner_a } - it { is_expected.to contain_exactly(runner_manager11, runner_manager12) } + it { is_expected.to contain_exactly(runner_manager_a1, runner_manager_a2) } end context 'with multiple runners' do - let(:runner_arg) { [runner1, runner2] } + let(:runner_arg) { [runner_a, runner_b] } - let_it_be(:runner2) { create(:ci_runner) } - let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner2) } + let_it_be(:runner_b) { create(:ci_runner) } + let_it_be(:runner_manager_b1) { create(:ci_runner_machine, runner: runner_b) } - it { is_expected.to contain_exactly(runner_manager11, runner_manager12, runner_manager2) } + it { is_expected.to contain_exactly(runner_manager_a1, runner_manager_a2, runner_manager_b1) } + end + end + + describe '.with_system_xid' do + subject(:runner_managers) { described_class.with_system_xid(system_xid) } + + let_it_be(:runner_a) { create(:ci_runner) } + let_it_be(:runner_b) { create(:ci_runner) } + let_it_be(:runner_manager_a1) { create(:ci_runner_machine, runner: runner_a, system_xid: 'id1') } + let_it_be(:runner_manager_a2) { create(:ci_runner_machine, runner: runner_a, system_xid: 'id2') } + let_it_be(:runner_manager_b1) { create(:ci_runner_machine, runner: runner_b, system_xid: 'id1') } + + context 'with single system id' do + let(:system_xid) { 'id2' } + + it { is_expected.to contain_exactly(runner_manager_a2) } + end + + context 'with multiple system ids' do + let(:system_xid) { %w[id1 id2] } + + it { is_expected.to contain_exactly(runner_manager_a1, runner_manager_a2, runner_manager_b1) } + end + + context 'when chained with another scope' do + subject(:runner_managers) { described_class.for_runner(runner).with_system_xid(system_xid) } + + let(:runner) { runner_a } + let(:system_xid) { 'id1' } + + it { is_expected.to contain_exactly(runner_manager_a1) } + + context 'with another runner' do + let(:runner) { runner_b } + + it { is_expected.to contain_exactly(runner_manager_b1) } + end end end @@ -146,18 +183,18 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo let!(:runner_version1) { create(:ci_runner_version, version: '16.0.0', status: :recommended) } let!(:runner_version2) { create(:ci_runner_version, version: '16.0.1', status: :available) } - let!(:runner1) { create(:ci_runner) } - let!(:runner2) { create(:ci_runner) } - let!(:runner_manager11) { create(:ci_runner_machine, runner: runner1, version: runner_version1.version) } - let!(:runner_manager12) { create(:ci_runner_machine, runner: runner1, version: runner_version2.version) } - let!(:runner_manager2) { create(:ci_runner_machine, runner: runner2, version: runner_version2.version) } + let!(:runner_a) { create(:ci_runner) } + let!(:runner_b) { create(:ci_runner) } + let!(:runner_manager_a1) { create(:ci_runner_machine, runner: runner_a, version: runner_version1.version) } + let!(:runner_manager_a2) { create(:ci_runner_machine, runner: runner_a, version: runner_version2.version) } + let!(:runner_manager_b1) { create(:ci_runner_machine, runner: runner_b, version: runner_version2.version) } subject { described_class.aggregate_upgrade_status_by_runner_id } it 'contains aggregate runner upgrade status by runner ID' do is_expected.to eq({ - runner1.id => :recommended, - runner2.id => :available + runner_a.id => :recommended, + runner_b.id => :available }) end end @@ -189,6 +226,108 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo it { is_expected.to eq([runner_manager2, runner_manager1]) } end + describe '.with_upgrade_status' do + subject(:scope) { described_class.with_upgrade_status(upgrade_status) } + + let_it_be(:runner_manager_14_0_0) { create(:ci_runner_machine, version: '14.0.0') } + let_it_be(:runner_manager_14_1_0) { create(:ci_runner_machine, version: '14.1.0') } + let_it_be(:runner_manager_14_1_1) { create(:ci_runner_machine, version: '14.1.1') } + + before_all do + create(:ci_runner_version, version: '14.0.0', status: :available) + create(:ci_runner_version, version: '14.1.0', status: :recommended) + create(:ci_runner_version, version: '14.1.1', status: :unavailable) + end + + context 'as :unavailable' do + let(:upgrade_status) { :unavailable } + + it 'returns runners with runner managers whose version is assigned :unavailable' do + is_expected.to contain_exactly(runner_manager_14_1_1) + end + end + + context 'as :available' do + let(:upgrade_status) { :available } + + it 'returns runners with runner managers whose version is assigned :available' do + is_expected.to contain_exactly(runner_manager_14_0_0) + end + end + + context 'as :recommended' do + let(:upgrade_status) { :recommended } + + it 'returns runners with runner managers whose version is assigned :recommended' do + is_expected.to contain_exactly(runner_manager_14_1_0) + end + end + end + + describe '.with_version_prefix' do + subject { described_class.with_version_prefix(version_prefix) } + + let_it_be(:runner_manager1) { create(:ci_runner_machine, version: '15.11.0') } + let_it_be(:runner_manager2) { create(:ci_runner_machine, version: '15.9.0') } + let_it_be(:runner_manager3) { create(:ci_runner_machine, version: '15.11.5') } + + context 'with a prefix string of "15."' do + let(:version_prefix) { "15." } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3) + end + end + + context 'with a prefix string of "15"' do + let(:version_prefix) { "15" } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3) + end + end + + context 'with a prefix string of "15.11."' do + let(:version_prefix) { "15.11." } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager1, runner_manager3) + end + end + + context 'with a prefix string of "15.11"' do + let(:version_prefix) { "15.11" } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager1, runner_manager3) + end + end + + context 'with a prefix string of "15.9"' do + let(:version_prefix) { "15.9" } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager2) + end + end + + context 'with a prefix string of "15.11.5"' do + let(:version_prefix) { "15.11.5" } + + it 'returns runner managers' do + is_expected.to contain_exactly(runner_manager3) + end + end + + context 'with a malformed prefix of "V2"' do + let(:version_prefix) { "V2" } + + it 'returns no runner managers' do + is_expected.to be_empty + end + end + end + describe '#status', :freeze_time do let(:runner_manager) { build(:ci_runner_machine, created_at: 8.days.ago) } @@ -425,106 +564,4 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo it { is_expected.to contain_exactly build } end end - - describe '.with_upgrade_status' do - subject(:scope) { described_class.with_upgrade_status(upgrade_status) } - - let_it_be(:runner_manager_14_0_0) { create(:ci_runner_machine, version: '14.0.0') } - let_it_be(:runner_manager_14_1_0) { create(:ci_runner_machine, version: '14.1.0') } - let_it_be(:runner_manager_14_1_1) { create(:ci_runner_machine, version: '14.1.1') } - - before_all do - create(:ci_runner_version, version: '14.0.0', status: :available) - create(:ci_runner_version, version: '14.1.0', status: :recommended) - create(:ci_runner_version, version: '14.1.1', status: :unavailable) - end - - context 'as :unavailable' do - let(:upgrade_status) { :unavailable } - - it 'returns runners with runner managers whose version is assigned :unavailable' do - is_expected.to contain_exactly(runner_manager_14_1_1) - end - end - - context 'as :available' do - let(:upgrade_status) { :available } - - it 'returns runners with runner managers whose version is assigned :available' do - is_expected.to contain_exactly(runner_manager_14_0_0) - end - end - - context 'as :recommended' do - let(:upgrade_status) { :recommended } - - it 'returns runners with runner managers whose version is assigned :recommended' do - is_expected.to contain_exactly(runner_manager_14_1_0) - end - end - end - - describe '.with_version_prefix' do - subject { described_class.with_version_prefix(version_prefix) } - - let_it_be(:runner_manager1) { create(:ci_runner_machine, version: '15.11.0') } - let_it_be(:runner_manager2) { create(:ci_runner_machine, version: '15.9.0') } - let_it_be(:runner_manager3) { create(:ci_runner_machine, version: '15.11.5') } - - context 'with a prefix string of "15."' do - let(:version_prefix) { "15." } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3) - end - end - - context 'with a prefix string of "15"' do - let(:version_prefix) { "15" } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager1, runner_manager2, runner_manager3) - end - end - - context 'with a prefix string of "15.11."' do - let(:version_prefix) { "15.11." } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager1, runner_manager3) - end - end - - context 'with a prefix string of "15.11"' do - let(:version_prefix) { "15.11" } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager1, runner_manager3) - end - end - - context 'with a prefix string of "15.9"' do - let(:version_prefix) { "15.9" } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager2) - end - end - - context 'with a prefix string of "15.11.5"' do - let(:version_prefix) { "15.11.5" } - - it 'returns runner managers' do - is_expected.to contain_exactly(runner_manager3) - end - end - - context 'with a malformed prefix of "V2"' do - let(:version_prefix) { "V2" } - - it 'returns no runner managers' do - is_expected.to be_empty - end - end - end end diff --git a/spec/models/organizations/organization_detail_spec.rb b/spec/models/organizations/organization_detail_spec.rb index 3f44a9cc637..dd49274e7dd 100644 --- a/spec/models/organizations/organization_detail_spec.rb +++ b/spec/models/organizations/organization_detail_spec.rb @@ -16,6 +16,15 @@ RSpec.describe Organizations::OrganizationDetail, type: :model, feature_category let(:model) { create(:organization_detail) } end + describe '#description_html' do + let_it_be(:model) { create(:organization_detail, description: '### Foo **Bar**') } + let(:expected_description) { ' Foo <strong>Bar</strong> ' } + + subject { model.description_html } + + it { is_expected.to eq_no_sourcepos(expected_description) } + end + context 'with uploads' do it_behaves_like 'model with uploads', false do let(:model_object) { create(:organization_detail) } diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb index 053a32281aa..aba2b03d4d1 100644 --- a/spec/models/organizations/organization_spec.rb +++ b/spec/models/organizations/organization_spec.rb @@ -59,6 +59,7 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel describe 'delegations' do it { is_expected.to delegate_method(:description).to(:organization_detail) } + it { is_expected.to delegate_method(:description_html).to(:organization_detail) } it { is_expected.to delegate_method(:avatar).to(:organization_detail) } it { is_expected.to delegate_method(:avatar_url).to(:organization_detail) } it { is_expected.to delegate_method(:remove_avatar!).to(:organization_detail) } diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index b4394f47105..11d906249e4 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -836,166 +836,219 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :fleet_v end describe 'GET /runners/:id/jobs' do - let_it_be(:job_1) { create(:ci_build) } - let_it_be(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) } - let_it_be(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) } - let_it_be(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) } - let_it_be(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) } - let(:path) { "/runners/#{project_runner.id}/jobs" } + subject(:request) { get api(path, user, **api_params) } + + let_it_be(:shared_runner_manager1) { create(:ci_runner_machine, runner: shared_runner, system_xid: 'id2') } + let_it_be(:jobs) do + project_runner_manager1 = create(:ci_runner_machine, runner: project_runner, system_xid: 'id1') + project_runner_manager2 = create(:ci_runner_machine, runner: two_projects_runner, system_xid: 'id1') + + [ + create(:ci_build), + create(:ci_build, :running, runner_manager: shared_runner_manager1, project: project), + create(:ci_build, :failed, runner_manager: shared_runner_manager1, project: project), + create(:ci_build, :running, runner_manager: project_runner_manager1, project: project), + create(:ci_build, :failed, runner_manager: project_runner_manager1, project: project), + create(:ci_build, :running, runner_manager: project_runner_manager2, project: project), + create(:ci_build, :running, runner_manager: project_runner_manager2, project: project2) + ] + end + + let(:api_params) { {} } + let(:runner_id) { project_runner.id } + let(:query_part) { query_params.merge(system_id_params).map { |param| param.join('=') }.join('&') } + let(:path) { "/runners/#{runner_id}/jobs?#{query_part}" } + let(:query_params) { {} } + let(:system_id_params) { {} } it_behaves_like 'GET request permissions for admin mode' context 'admin user' do + let(:user) { admin } + let(:api_params) { { admin_mode: true } } + context 'when runner exists' do context 'when runner is shared' do + let(:runner_id) { shared_runner.id } + let(:system_id) { 'id2' } + it 'return jobs' do - get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(2) + expect(json_response).to match([ + a_hash_including('id' => jobs[1].id), + a_hash_including('id' => jobs[2].id) + ]) + end + + it_behaves_like 'an endpoint with keyset pagination', invalid_order: nil do + let(:first_record) { jobs[2] } + let(:second_record) { jobs[1] } + let(:api_call) { api(path, user, **api_params) } end end context 'when runner is a project runner' do it 'return jobs' do - get api(path, admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(2) + expect(json_response).to match([ + a_hash_including('id' => jobs[3].id), + a_hash_including('id' => jobs[4].id) + ]) end context 'when user does not have authorization to see all jobs' do - it 'shows only jobs it has permission to see' do - create(:ci_build, :running, runner: two_projects_runner, project: project) - create(:ci_build, :running, runner: two_projects_runner, project: project2) + let(:runner_id) { two_projects_runner.id } + let(:user) { user2 } + let(:api_params) { {} } + before_all do project.add_guest(user2) project2.add_maintainer(user2) - get api("/runners/#{two_projects_runner.id}/jobs", user2) + end + + it 'shows only jobs it has permission to see' do + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(1) + expect(json_response).to match([a_hash_including('id' => jobs[6].id)]) end end end context 'when valid status is provided' do + let(:query_params) { { status: :failed } } + it 'return filtered jobs' do - get api("/runners/#{project_runner.id}/jobs?status=failed", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(1) - expect(json_response.first).to include('id' => job_5.id) + expect(json_response).to match([a_hash_including('id' => jobs[4].id)]) end end context 'when valid order_by is provided' do + let(:query_params) { { order_by: :id } } + context 'when sort order is not specified' do it 'return jobs in descending order' do - get api("/runners/#{project_runner.id}/jobs?order_by=id", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(2) - expect(json_response.first).to include('id' => job_5.id) + expect(json_response).to match([ + a_hash_including('id' => jobs[4].id), + a_hash_including('id' => jobs[3].id) + ]) end end context 'when sort order is specified as asc' do + let(:query_params) { { order_by: :id, sort: :asc } } + it 'return jobs sorted in ascending order' do - get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(2) - expect(json_response.first).to include('id' => job_4.id) + expect(json_response).to match([ + a_hash_including('id' => jobs[3].id), + a_hash_including('id' => jobs[4].id) + ]) end end end context 'when invalid status is provided' do + let(:query_params) { { status: 'non-existing' } } + it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:bad_request) end end context 'when invalid order_by is provided' do + let(:query_params) { { order_by: 'non-existing' } } + it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:bad_request) end end context 'when invalid sort is provided' do + let(:query_params) { { sort: 'non-existing' } } + it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:bad_request) end end end - it 'avoids N+1 DB queries' do - get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) + describe 'eager loading' do + let(:runner_id) { shared_runner.id } - control = ActiveRecord::QueryRecorder.new do - get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) - end + it 'avoids N+1 DB queries' do + get api(path, user, **api_params) - create(:ci_build, :failed, runner: shared_runner, project: project) + control = ActiveRecord::QueryRecorder.new do + get api(path, user, **api_params) + end - expect do - get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) - end.not_to exceed_query_limit(control) - end + create(:ci_build, :failed, runner: shared_runner, project: project) - it 'batches loading of commits' do - shared_runner = create(:ci_runner, :instance, description: 'Shared runner') + expect do + get api(path, user, **api_params) + end.not_to exceed_query_limit(control.count) + end - project_with_repo = create(:project, :repository) + it 'batches loading of commits' do + project_with_repo = create(:project, :repository) + shared_runner_manager1 = create(:ci_runner_machine, runner: shared_runner, system_xid: 'id1') - pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b') - create(:ci_build, :running, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b') + create(:ci_build, :running, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline) - pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478') - create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478') + create(:ci_build, :failed, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline) - pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') - create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline) + pipeline = create(:ci_pipeline, project: project_with_repo, sha: '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + create(:ci_build, :failed, runner_manager: shared_runner_manager1, project: project_with_repo, pipeline: pipeline) - expect_next_instance_of(Repository) do |repo| - expect(repo).to receive(:commits_by).with(oids: - %w[ - 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 - c1c67abbaf91f624347bb3ae96eabe3a1b742478 - ]).once.and_call_original - end + expect_next_instance_of(Repository) do |repo| + expect(repo).to receive(:commits_by).with(oids: + %w[ + 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 + c1c67abbaf91f624347bb3ae96eabe3a1b742478 + ]).once.and_call_original + end - get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true), params: { per_page: 2, order_by: 'id', sort: 'desc' } + get api(path, admin, admin_mode: true), params: { per_page: 2, order_by: 'id', sort: 'desc' } + end end context "when runner doesn't exist" do + let(:runner_id) { non_existing_record_id } + it 'returns 404' do - get api('/runners/0/jobs', admin, admin_mode: true) + request expect(response).to have_gitlab_http_status(:not_found) end @@ -1004,70 +1057,118 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :fleet_v context "runner project's administrative user" do context 'when runner exists' do + let(:runner_id) { shared_runner.id } + context 'when runner is shared' do it 'returns 403' do - get api("/runners/#{shared_runner.id}/jobs", user) + request expect(response).to have_gitlab_http_status(:forbidden) end end context 'when runner is a project runner' do + let(:runner_id) { project_runner.id } + it 'return jobs' do - get api(path, user) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(2) + expect(json_response).to match([ + a_hash_including('id' => jobs[3].id), + a_hash_including('id' => jobs[4].id) + ]) end - end - context 'when valid status is provided' do - it 'return filtered jobs' do - get api("/runners/#{project_runner.id}/jobs?status=failed", user) + context 'when valid status is provided' do + let(:query_params) { { status: :failed } } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers + it 'return filtered jobs' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(1) - expect(json_response.first).to include('id' => job_5.id) + expect(json_response).to match([ + a_hash_including('id' => jobs[4].id) + ]) + end end - end - context 'when invalid status is provided' do - it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?status=non-existing", user) + context 'when invalid status is provided' do + let(:query_params) { { status: 'non-existing' } } - expect(response).to have_gitlab_http_status(:bad_request) + it 'return 400' do + request + + expect(response).to have_gitlab_http_status(:bad_request) + end end end end context "when runner doesn't exist" do + let(:runner_id) { non_existing_record_id } + it 'returns 404' do - get api('/runners/0/jobs', user) + request expect(response).to have_gitlab_http_status(:not_found) end end - end - context 'other authorized user' do - it 'does not return jobs' do - get api(path, user2) + context 'other authorized user' do + let(:user) { user2 } - expect(response).to have_gitlab_http_status(:forbidden) + it 'does not return jobs' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'unauthorized user' do + let(:user) { nil } + + it 'does not return jobs' do + request + + expect(response).to have_gitlab_http_status(:unauthorized) + end end end - context 'unauthorized user' do - it 'does not return jobs' do - get api(path) + context 'with system_id param' do + let(:system_id_params) { { system_id: system_id } } + let(:system_id) { 'id1' } + let(:user) { admin } + let(:api_params) { { admin_mode: true } } - expect(response).to have_gitlab_http_status(:unauthorized) + it 'returns jobs from the runner manager' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_limited_pagination_headers + expect(response.headers).not_to include('X-Total', 'X-Total-Pages') + + expect(json_response).to match([ + a_hash_including('id' => jobs[3].id), + a_hash_including('id' => jobs[4].id) + ]) + end + + context 'when system_id does not match runner' do + let(:runner_id) { shared_runner.id } + + it 'does not return jobs' do + request + + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response).to be_empty + end end end end diff --git a/spec/requests/organizations/organizations_controller_spec.rb b/spec/requests/organizations/organizations_controller_spec.rb index bfd0603eb3d..381e7f98dbe 100644 --- a/spec/requests/organizations/organizations_controller_spec.rb +++ b/spec/requests/organizations/organizations_controller_spec.rb @@ -119,4 +119,28 @@ RSpec.describe Organizations::OrganizationsController, feature_category: :cell d it_behaves_like 'controller action that requires authentication by any user' end + + describe 'POST #preview_markdown' do + subject(:gitlab_request) { post preview_markdown_organizations_path, params: { text: '### Foo \n **bar**' } } + + it_behaves_like 'controller action that requires authentication by any user' + + context 'when the user is signed in' do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'returns html from markdown' do + sign_in(user) + gitlab_request + + body = Gitlab::Json.parse(response.body)['body'] + + expect(body).not_to include('Foo</h3>') + expect(body).to include('<strong>bar</strong>') + end + end + end end diff --git a/spec/routing/organizations/organizations_controller_routing_spec.rb b/spec/routing/organizations/organizations_controller_routing_spec.rb index f105bb31ccf..c5c7a0ae377 100644 --- a/spec/routing/organizations/organizations_controller_routing_spec.rb +++ b/spec/routing/organizations/organizations_controller_routing_spec.rb @@ -29,4 +29,9 @@ RSpec.describe Organizations::OrganizationsController, :routing, feature_categor expect(get("/-/organizations/#{organization.path}/users")) .to route_to('organizations/organizations#users', organization_path: organization.path) end + + it 'routes to #preview_markdown' do + expect(post("/-/organizations/preview_markdown")) + .to route_to('organizations/organizations#preview_markdown') + end end diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb index 43ac9457ed6..50049d76f0e 100644 --- a/spec/rubocop/cop/scalability/file_uploads_spec.rb +++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb @@ -4,7 +4,7 @@ require 'rubocop_spec_helper' require_relative '../../../../rubocop/cop/scalability/file_uploads' RSpec.describe RuboCop::Cop::Scalability::FileUploads, feature_category: :scalability do - let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' } + let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads/' } context 'with required params' do it 'detects File in types array' do diff --git a/spec/support/helpers/debug_with_puts.rb b/spec/support/helpers/debug_with_puts.rb new file mode 100644 index 00000000000..b8599cc7d40 --- /dev/null +++ b/spec/support/helpers/debug_with_puts.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# TODO: Remove the debug_with_puts statements below! Used for debugging purposes. +# TODO: https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/323#note_1688925316 +module DebugWithPuts + def debug_with_puts(message) + return unless ENV['CI'] # rubocop:disable RSpec/AvoidConditionalStatements -- Debug information only in the CI + + warn "[#{Time.current}] #{message}" + end + + module_function :debug_with_puts +end diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb index bde5535705e..b77b366e037 100644 --- a/spec/support/helpers/stub_requests.rb +++ b/spec/support/helpers/stub_requests.rb @@ -18,15 +18,15 @@ module StubRequests end def stub_dns(url, ip_address:, port: 80) - debug_with_puts "beginning of stub_dns" + DebugWithPuts.debug_with_puts "beginning of stub_dns" url = parse_url(url) - debug_with_puts "before socket = Socket.sockaddr_in" + DebugWithPuts.debug_with_puts "before socket = Socket.sockaddr_in" socket = Socket.sockaddr_in(port, ip_address) - debug_with_puts "after socket = Socket.sockaddr_in" + DebugWithPuts.debug_with_puts "after socket = Socket.sockaddr_in" - debug_with_puts "before addr = Addrinfo.new(socket)" + DebugWithPuts.debug_with_puts "before addr = Addrinfo.new(socket)" addr = Addrinfo.new(socket) - debug_with_puts "after addr = Addrinfo.new(socket)" + DebugWithPuts.debug_with_puts "after addr = Addrinfo.new(socket)" # See Gitlab::UrlBlocker allow(Addrinfo).to receive(:getaddrinfo) @@ -58,12 +58,4 @@ module StubRequests def parse_url(url) url.is_a?(URI) ? url : URI(url) end - - # TODO: Remove the debug_with_puts statements below! Used for debugging purposes. - # TODO: https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/323#note_1688925316 - def debug_with_puts(message) - return unless ENV['CI'] # rubocop:disable RSpec/AvoidConditionalStatements -- Debug information only in the CI - - puts "[#{Time.current}] #{message}" - end end diff --git a/vendor/gems/bundler-checksum/Gemfile.lock b/vendor/gems/bundler-checksum/Gemfile.lock index 8ae053f0105..4db0507a63b 100644 --- a/vendor/gems/bundler-checksum/Gemfile.lock +++ b/vendor/gems/bundler-checksum/Gemfile.lock @@ -15,4 +15,4 @@ DEPENDENCIES bundler-checksum! BUNDLED WITH - 2.3.17 + 2.5.4 diff --git a/vendor/gems/bundler-checksum/lib/bundler_checksum.rb b/vendor/gems/bundler-checksum/lib/bundler_checksum.rb index b3d36521f24..083082c0ab1 100644 --- a/vendor/gems/bundler-checksum/lib/bundler_checksum.rb +++ b/vendor/gems/bundler-checksum/lib/bundler_checksum.rb @@ -41,12 +41,18 @@ module Bundler raise "#{@package.inspect} does not have :@gem" unless source raise "#{source.inspect} does not respond to :with_read_io" unless source.respond_to?(:with_read_io) - digest = source.with_read_io do |io| - digest = SharedHelpers.digest(:SHA256).new - digest << io.read(16_384) until io.eof? - io.rewind - send(checksum_type(checksum), digest) - end + digest = + if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new("2.5.0") + gem_checksum.digest + else + source.with_read_io do |io| + digest = SharedHelpers.digest(:SHA256).new + digest << io.read(16_384) until io.eof? + io.rewind + send(checksum_type(checksum), digest) + end + end + unless digest == checksum raise SecurityError, <<-MESSAGE Bundler cannot continue installing #{spec.name} (#{spec.version}). diff --git a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile.lock b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile.lock index d633184e300..2aa6a15070f 100644 --- a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile.lock +++ b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile.lock @@ -135,4 +135,4 @@ DEPENDENCIES rails (~> 6.1.6.1) BUNDLED WITH - 2.3.22 + 2.5.4 |