Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-26 00:20:33 +00:00
parent 5acb134776
commit 2d3e590602
51 changed files with 808 additions and 292 deletions

View File

@ -1079,7 +1079,6 @@ Layout/LineLength:
- 'ee/lib/ee/api/helpers/settings_helpers.rb'
- 'ee/lib/ee/api/helpers/users_helpers.rb'
- 'ee/lib/ee/api/internal/base.rb'
- 'ee/lib/ee/api/internal/kubernetes.rb'
- 'ee/lib/ee/api/issues.rb'
- 'ee/lib/ee/api/members.rb'
- 'ee/lib/ee/api/merge_request_approvals.rb'

View File

@ -140,7 +140,7 @@ gem 'grape-path-helpers', '~> 2.0.1', feature_category: :api
gem 'rack-cors', '~> 2.0.1', require: 'rack/cors' # rubocop:todo Gemfile/MissingFeatureCategory
# GraphQL API
gem 'graphql', '~> 2.3.4', feature_category: :api
gem 'graphql', '~> 2.3.5', feature_category: :api
gem 'graphql-docs', '~> 4.0.0', group: [:development, :test], feature_category: :api
gem 'graphiql-rails', '~> 1.8.0', feature_category: :api
gem 'apollo_upload_server', '~> 2.1.6', feature_category: :api
@ -561,7 +561,7 @@ group :test do
gem 'gitlab_quality-test_tooling', '~> 1.29.1', require: false, feature_category: :tooling
end
gem 'octokit', '~> 8.1', feature_category: :importers
gem 'octokit', '~> 9.0', feature_category: :importers
gem 'gitlab-mail_room', '~> 0.0.24', require: 'mail_room', feature_category: :shared

View File

@ -279,7 +279,7 @@
{"name":"graphiql-rails","version":"1.8.0","platform":"ruby","checksum":"02e2c5098be2c6c29219a0e9b2910a2cd3c494301587a3199a7c4484d8038ed1"},
{"name":"graphlient","version":"0.6.0","platform":"ruby","checksum":"b8d8664b4c8ec215012cbe3cca918a045b0a206d709712d68b6db51fd215c5c0"},
{"name":"graphlyte","version":"1.0.0","platform":"ruby","checksum":"b5af4ab67dde6e961f00ea1c18f159f73b52ed11395bb4ece297fe628fa1804d"},
{"name":"graphql","version":"2.3.4","platform":"ruby","checksum":"020f313608237723e576e2621ab469a54087eaac2240261f9e41478d2725c935"},
{"name":"graphql","version":"2.3.5","platform":"ruby","checksum":"9c367835f86541660d24c3d81632267ecee553d304577aaee070f8ac05860af1"},
{"name":"graphql-client","version":"0.19.0","platform":"ruby","checksum":"fe699d81976f916bd8f989216155326449cb8475a5d69fa1dd054012a86969c7"},
{"name":"graphql-docs","version":"4.0.0","platform":"ruby","checksum":"f68296959263db26e1b7ba7058856d67b641cf508187222268be58f09dfa02d7"},
{"name":"grpc","version":"1.63.0","platform":"aarch64-linux","checksum":"dc75c5fd570b819470781d9512105dddfdd11d984f38b8e60bb946f92d1f79ee"},
@ -355,7 +355,7 @@
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
{"name":"license_finder","version":"7.1.0","platform":"ruby","checksum":"6d020b3639f74da1488ddff052b3c93410cbf89a82dc884d404caa5ad072c66c"},
{"name":"licensee","version":"9.16.1","platform":"ruby","checksum":"04c0b57d20b91fa82ac9dcb3637da9cdc1c3823e217c0781e5d0514958e2e515"},
{"name":"licensee","version":"9.17.1","platform":"ruby","checksum":"0be022c66d8853d35b08171a0f2575d5ccb5aef8a7020a1815938b4f26f4089a"},
{"name":"listen","version":"3.7.1","platform":"ruby","checksum":"3b80caa7aa77fae836916c2f9e3fbcafbd15f5d695dd487c1f5b5e7e465efe29"},
{"name":"llhttp-ffi","version":"0.4.0","platform":"ruby","checksum":"e5f7327db3cf8007e648342ef76347d6e0ae545a8402e519cca9c886eb37b001"},
{"name":"locale","version":"2.1.3","platform":"ruby","checksum":"b6ddee011e157817cb98e521b3ce7cb626424d5882f1e844aafdee3e8b212725"},
@ -426,7 +426,7 @@
{"name":"numerizer","version":"0.2.0","platform":"ruby","checksum":"e58076d5ee5370417b7e52d9cb25836d62acd1b8d9a194c308707986c1705d7b"},
{"name":"oauth","version":"0.5.6","platform":"ruby","checksum":"4085fe28e0c5e2434135e00a6555294fd2a4ff96a98d1bdecdcd619fc6368dff"},
{"name":"oauth2","version":"2.0.9","platform":"ruby","checksum":"b21f9defcf52dc1610e0dfab4c868342173dcd707fd15c777d9f4f04e153f7fb"},
{"name":"octokit","version":"8.1.0","platform":"ruby","checksum":"82229ce9b54e910e27ae75ff21e54bc97072913b5af06750999966e6817af8cd"},
{"name":"octokit","version":"9.1.0","platform":"ruby","checksum":"7849a659d2722c629181f48d1d7e567c9539f1a85c9676144dbdbfc6ce288253"},
{"name":"ohai","version":"18.1.3","platform":"ruby","checksum":"980cfd6a6597f897e157532ba2168d29afb83a8f5e125f682ec3248c3407df95"},
{"name":"oj","version":"3.13.23","platform":"ruby","checksum":"206dfdc4020ad9974705037f269cfba211d61b7662a58c717cce771829ccef51"},
{"name":"oj-introspect","version":"0.7.2","platform":"ruby","checksum":"c415a44567ed2870d8e963a69421d9322128e194fab7867e37e54d5a25d5333d"},
@ -649,7 +649,7 @@
{"name":"spring-commands-rspec","version":"1.0.4","platform":"ruby","checksum":"6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c"},
{"name":"sprite-factory","version":"1.7.1","platform":"ruby","checksum":"5586524a1aec003241f1abc6852b61433e988aba5ee2b55f906387bf49b01ba2"},
{"name":"sprockets","version":"3.7.2","platform":"ruby","checksum":"5ea1d7facd09203c1aa196afd6178208cd25abdbcc2a9978810a2f0754e152a0"},
{"name":"sprockets-rails","version":"3.4.2","platform":"ruby","checksum":"36d6327757ccf7460a00d1d52b2d5ef0019a4670503046a129fa1fb1300931ad"},
{"name":"sprockets-rails","version":"3.5.1","platform":"ruby","checksum":"c44626cb3887a1a8b572ca258685db33b4ebd041aa73428a716eac444ee5ef48"},
{"name":"ssh_data","version":"1.3.0","platform":"ruby","checksum":"ec7c1e95a3aebeee412147998f4c147b4b05da6ed0aafda6083f9449318eaac0"},
{"name":"ssrf_filter","version":"1.0.8","platform":"ruby","checksum":"03f49f54837e407d43ee93ec733a8a94dc1bcf8185647ac61606e63aaedaa0db"},
{"name":"stackprof","version":"0.2.25","platform":"ruby","checksum":"28db0e2d22b817ae35def7163822505a04a026b02ef119b6aa89d70b967b0d2e"},

View File

@ -876,7 +876,7 @@ GEM
faraday_middleware
graphql-client
graphlyte (1.0.0)
graphql (2.3.4)
graphql (2.3.5)
base64
graphql-client (0.19.0)
activesupport (>= 3.0)
@ -1042,9 +1042,9 @@ GEM
tomlrb (>= 1.3, < 2.1)
with_env (= 1.1.0)
xml-simple (~> 1.1.9)
licensee (9.16.1)
dotenv (~> 2.0)
octokit (>= 4.20, < 9.0)
licensee (9.17.1)
dotenv (>= 2, < 4)
octokit (>= 4.20, < 10.0)
reverse_markdown (>= 1, < 3)
rugged (>= 0.24, < 2.0)
thor (>= 0.19, < 2.0)
@ -1156,8 +1156,7 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
octokit (8.1.0)
base64
octokit (9.1.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
ohai (18.1.3)
@ -1724,9 +1723,9 @@ GEM
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets-rails (3.5.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
ssh_data (1.3.0)
ssrf_filter (1.0.8)
@ -2053,7 +2052,7 @@ DEPENDENCIES
graphiql-rails (~> 1.8.0)
graphlient (~> 0.6.0)
graphlyte (~> 1.0.0)
graphql (~> 2.3.4)
graphql (~> 2.3.5)
graphql-docs (~> 4.0.0)
grpc (~> 1.63)
gssapi (~> 1.3.1)
@ -2107,7 +2106,7 @@ DEPENDENCIES
net-protocol (~> 0.1.3)
nokogiri (~> 1.16)
oauth2 (~> 2.0)
octokit (~> 8.1)
octokit (~> 9.0)
ohai (~> 18.1)
oj (~> 3.13.21)
oj-introspect (~> 0.7)

View File

@ -85,7 +85,8 @@ export default {
},
update({ project: { jobs: { nodes = [], pageInfo = {} } = {} } }) {
this.pageInfo = pageInfo;
return nodes
const jobNodes = nodes
.map(mapArchivesToJobNodes)
.map(mapBooleansToJobNodes)
.map((jobNode) => {
@ -96,6 +97,12 @@ export default {
_showDetails: this.expandedJobs.includes(jobNode.id),
};
});
if (jobNodes.some((jobNode) => !jobNode.hasArtifacts)) {
this.$apollo.queries.jobArtifacts.refetch();
}
return jobNodes;
},
error() {
createAlert({
@ -367,6 +374,9 @@ export default {
createdLabel: I18N_CREATED,
artifactsCount: I18N_ARTIFACTS_COUNT,
},
TBODY_TR_ATTR: {
'data-testid': 'job-artifact-table-row',
},
};
</script>
<template>
@ -391,6 +401,7 @@ export default {
:busy="$apollo.queries.jobArtifacts.loading"
stacked="sm"
details-td-class="gl-bg-gray-10! gl-p-0! gl-overflow-auto"
:tbody-tr-attr="$options.TBODY_TR_ATTR"
>
<template #table-busy>
<gl-skeleton-loader v-for="i in 20" :key="i" :width="1000" :height="75">

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import PipelineEditorApp from 'jh_else_ce/ci/pipeline_editor/pipeline_editor_app.vue';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
@ -11,7 +12,6 @@ import getLastCommitBranch from './graphql/queries/client/last_commit_branch.que
import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql';
import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
export const createAppOptions = (el) => {
const {

View File

@ -18,7 +18,7 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
import { ALL_SCOPE, SCHEDULES_PER_PAGE, DEFAULT_SORT_VALUE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
@ -90,6 +90,7 @@ export default {
// we need to ensure we send null to the API when
// the scope is 'ALL'
status: this.scope === ALL_SCOPE ? null : this.scope,
sortValue: this.sortValue,
first: this.pagination.first,
last: this.pagination.last,
prevPageCursor: this.pagination.prevPageCursor,
@ -128,6 +129,9 @@ export default {
playSuccess: false,
errorMessage: '',
scheduleId: null,
sortValue: DEFAULT_SORT_VALUE,
sortBy: 'ID',
sortDesc: true,
showDeleteModal: false,
showTakeOwnershipModal: false,
count: 0,
@ -332,6 +336,13 @@ export default {
};
}
},
onUpdateSorting(sortValue, sortBy, sortDesc) {
this.sortValue = sortValue;
this.sortBy = sortBy;
this.sortDesc = sortDesc;
this.resetPagination();
},
},
};
</script>
@ -397,9 +408,12 @@ export default {
<pipeline-schedules-table
:schedules="schedules.list"
:current-user="schedules.currentUser"
:sort-by="sortBy"
:sort-desc="sortDesc"
@showTakeOwnershipModal="setTakeOwnershipModal"
@showDeleteModal="setDeleteModal"
@playPipelineSchedule="playPipelineSchedule"
@update-sorting="onUpdateSorting"
/>
<gl-pagination

View File

@ -1,6 +1,7 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import { TH_DESCRIPTION_TEST_ID, TH_TARGET_TEST_ID, TH_NEXT_TEST_ID } from '../../constants';
import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue';
@ -14,15 +15,21 @@ export default {
fields: [
{
key: 'description',
actualSortKey: 'DESCRIPTION',
label: s__('PipelineSchedules|Description'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-8/20',
sortable: true,
thAttr: TH_DESCRIPTION_TEST_ID,
},
{
key: 'target',
actualSortKey: 'REF',
sortable: true,
label: s__('PipelineSchedules|Target'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-2/20',
thAttr: TH_TARGET_TEST_ID,
},
{
key: 'pipeline',
@ -32,9 +39,12 @@ export default {
},
{
key: 'next',
actualSortKey: 'NEXT_RUN_AT',
label: s__('PipelineSchedules|Next Run'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-3/20',
sortable: true,
thAttr: TH_NEXT_TEST_ID,
},
{
key: 'owner',
@ -66,6 +76,24 @@ export default {
type: Object,
required: true,
},
sortBy: {
type: String,
required: true,
},
sortDesc: {
type: Boolean,
required: true,
},
},
methods: {
fetchSortedData({ sortBy, sortDesc }) {
const field = this.$options.fields.find(({ key }) => key === sortBy);
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
if (!field?.actualSortKey) return;
this.$emit('update-sorting', `${field.actualSortKey}_${sortingDirection}`, sortBy, sortDesc);
},
},
};
</script>
@ -76,8 +104,11 @@ export default {
:items="schedules"
:tbody-tr-attr="{ 'data-testid': 'pipeline-schedule-table-row' }"
:empty-text="$options.i18n.emptyText"
:sort-by="sortBy"
:sort-desc="sortDesc"
show-empty
stacked="md"
@sort-changed="fetchSortedData"
>
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />

View File

@ -2,3 +2,8 @@ export const VARIABLE_TYPE = 'ENV_VAR';
export const FILE_TYPE = 'FILE';
export const ALL_SCOPE = 'ALL';
export const SCHEDULES_PER_PAGE = 50;
export const DEFAULT_SORT_VALUE = 'ID_DESC';
export const TH_DESCRIPTION_TEST_ID = { 'data-testid': 'pipeline-schedules-description-sort' };
export const TH_TARGET_TEST_ID = { 'data-testid': 'pipeline-schedules-target-sort' };
export const TH_NEXT_TEST_ID = { 'data-testid': 'pipeline-schedules-next-sort' };

View File

@ -4,6 +4,7 @@ query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
$sortValue: PipelineScheduleSort
$first: Int
$last: Int
$prevPageCursor: String = ""
@ -21,6 +22,7 @@ query getPipelineSchedulesQuery(
pipelineSchedules(
status: $status
ids: $ids
sort: $sortValue
first: $first
last: $last
after: $nextPageCursor

View File

@ -90,12 +90,8 @@ export default {
<template>
<li :class="{ 'js-toggle-container': collapsible }" class="commit">
<div
class="gl-block sm:gl-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse"
>
<div
class="commit-actions flex-row gl-hidden sm:gl-flex gl-align-items-center gl-flex-wrap justify-content-end"
>
<div class="gl-block sm:gl-flex gl-flex-row-reverse gl-justify-between gl-items-start">
<div class="commit-actions gl-flex-row gl-hidden sm:gl-flex gl-items-center gl-justify-end">
<div
v-if="commit.signature_html"
v-html="commit.signature_html /* eslint-disable-line vue/no-v-html */"

View File

@ -74,6 +74,7 @@ module Packages
def current_package_protected?
return false if Feature.disabled?(:packages_protected_packages, project)
return false if current_user.is_a?(DeployToken)
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
project.package_protection_rules.for_push_exists?(access_level: user_project_authorization_access_level,

View File

@ -30,6 +30,7 @@
= paginate @projects, theme: 'gitlab'
- else
.nothing-here-block= _('No projects found')
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-projects-md.svg',
title: _('No projects found'))
#delete-project-modal

View File

@ -7,19 +7,24 @@
.js-jh-transition-banner{ data: { feature_name: Users::CalloutsHelper::TRANSITION_TO_JIHU_CALLOUT,
user_preferred_language: current_user.preferred_language} }
.top-area.gl-flex-direction-column-reverse
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
= render ::Layouts::PageHeadingComponent.new(_('Projects')) do |c|
- c.with_actions do
= link_button_to new_project_path, variant: :confirm do
= _('New Project')
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-grow.gl-basis-0.gl-min-w-0
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-grow gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do
= gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
= gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
= gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
= gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
.nav-controls
= render 'shared/projects/search_form', autofocus: true, admin_view: true
.md:gl-flex.gl-min-w-0.gl-grow.gl-border-t-0.row-content-block
= render 'shared/projects/search_form', admin_view: true
= render 'projects'

View File

@ -36,11 +36,11 @@
= stylesheet_link_tag_defer "application_utilities_dark"
- elsif user_application_system_mode?
%meta{ name: 'color-scheme', content: 'light dark' }
= stylesheet_link_tag "application", media: "(prefers-color-scheme: light)"
= stylesheet_link_tag "application_dark", media: "(prefers-color-scheme: dark)"
= universal_stylesheet_link_tag "application", media: "(prefers-color-scheme: light)"
= universal_stylesheet_link_tag "application_dark", media: "(prefers-color-scheme: dark)"
= yield :page_specific_styles
= stylesheet_link_tag "application_utilities", media: "(prefers-color-scheme: light)"
= stylesheet_link_tag "application_utilities_dark", media: "(prefers-color-scheme: dark)"
= universal_stylesheet_link_tag "application_utilities", media: "(prefers-color-scheme: light)"
= universal_stylesheet_link_tag "application_utilities_dark", media: "(prefers-color-scheme: dark)"
- else
= stylesheet_link_tag_defer "application"
= yield :page_specific_styles

View File

@ -1,11 +1,10 @@
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : _('Filter by name')
- admin_view ||= false
- top_padding = admin_view ? 'gl-lg-pt-3' : ''
= form_tag filter_projects_path, method: :get, class: "project-filter-form gl-display-flex! gl-flex-wrap gl-w-full gl-gap-3 #{top_padding}", data: { testid: 'project-filter-form-container' }, id: 'project-filter-form' do |f|
= form_tag filter_projects_path, method: :get, class: "project-filter-form !gl-flex gl-flex-wrap gl-w-full gl-gap-3", data: { testid: 'project-filter-form-container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
class: "project-filter-form-field form-control input-short js-projects-list-filter gl-m-0!",
class: "project-filter-form-field form-control input-short js-projects-list-filter !gl-m-0 gl-grow",
spellcheck: false,
id: 'project-filter-form-field',
autofocus: local_assigns[:autofocus]
@ -28,7 +27,7 @@
- if params[:language].present?
= hidden_field_tag :language, params[:language]
.dropdown{ class: 'gl-m-0!' }
.dropdown{ class: '!gl-m-0' }
= dropdown_toggle(search_language_placeholder, { toggle: 'dropdown', testid: 'project-language-dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li
@ -48,8 +47,5 @@
- if params[:namespace_id].present?
- namespace = Namespace.find(params[:namespace_id])
- selected_text = "#{namespace.kind}: #{namespace.full_path}" if namespace
.gl-display-flex.gl-w-full.gl-md-w-auto{ class: 'gl-m-0!' }
.gl-flex.gl-w-full.gl-md-w-auto{ class: '!gl-m-0' }
.js-namespace-select{ data: { field_name: 'namespace_id', selected_id: namespace&.id, selected_text: selected_text, update_location: 'true' } }
= link_button_to new_project_path, class: 'gl-display-inline gl-mb-0!', variant: :confirm do
= _('New Project')

View File

@ -40,6 +40,13 @@ To move databases from one instance to another:
/opt/gitlab/embedded/bin/pg_dump -h $SRC_PGHOST -U $SRC_PGUSER -c -C -f praefect_production.sql praefect_production
```
NOTE:
In rare occasions, you might notice database performance issues after you perform
a `pg_dump` and restore. This can happen because `pg_dump` does not contain the statistics
[used by the optimizer to make query planning decisions](https://www.postgresql.org/docs/14/app-pgdump.html).
If performance degrades after a restore, fix the problem by finding the problematic query,
then running ANALYZE on the tables used by the query.
1. Restore the databases to the destination (this overwrites any existing databases with the same names):
```shell

View File

@ -1777,17 +1777,19 @@ Refer to the [external Sidekiq documentation](../sidekiq/index.md) for more info
To configure the Sidekiq nodes, on each one:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -1783,17 +1783,19 @@ Refer to the [external Sidekiq documentation](../sidekiq/index.md) for more info
To configure the Sidekiq nodes, on each one:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -625,17 +625,19 @@ Refer to the [external Sidekiq documentation](../sidekiq/index.md) for more info
To configure the Sidekiq server, on the server node you want to use for Sidekiq:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -1617,17 +1617,19 @@ The following IPs will be used as an example:
To configure the Sidekiq nodes, on each one:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -1793,17 +1793,19 @@ Refer to the [external Sidekiq documentation](../sidekiq/index.md) for more info
To configure the Sidekiq nodes, on each one:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -1613,17 +1613,19 @@ Refer to the [external Sidekiq documentation](../sidekiq/index.md) for more info
To configure the Sidekiq nodes, on each one:
1. SSH in to the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux package
package of your choice. Be sure to follow _only_ installation steps 1 and 2
on the page.
1. Create or edit `/etc/gitlab/gitlab.rb` and use the following configuration:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])

View File

@ -17,25 +17,12 @@ PostgreSQL, and Gitaly instances.
By default, GitLab uses UNIX sockets and is not set up to communicate via TCP. To change this:
1. [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip) adding the Sidekiq server IP addresses to `postgresql['md5_auth_cidr_addresses']`
1. [Make the bundled Redis reachable via TCP](https://docs.gitlab.com/omnibus/settings/redis.html#making-the-bundled-redis-reachable-via-tcp)
1. Edit the `/etc/gitlab/gitlab.rb` file on your GitLab instance, and add the following:
```ruby
## PostgreSQL
# Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
postgresql['listen_address'] = '0.0.0.0'
postgresql['port'] = 5432
# Add the Sidekiq nodes to PostgreSQL's trusted addresses.
# In the following example, 10.10.1.30/32 is the private IP
# of the Sidekiq server.
postgresql['md5_auth_cidr_addresses'] = %w(127.0.0.1/32 10.10.1.30/32)
postgresql['trust_auth_cidr_addresses'] = %w(127.0.0.1/32 10.10.1.30/32)
## Gitaly
gitaly['configuration'] = {
# ...
#
@ -48,18 +35,10 @@ By default, GitLab uses UNIX sockets and is not set up to communicate via TCP. T
},
}
gitaly['auth_token'] = ''
praefect['configuration'][:auth][:token] = 'abc123secret'
gitlab_rails['gitaly_token'] = 'abc123secret'
## Redis configuration
redis['bind'] = '0.0.0.0'
redis['port'] = 6379
# Password to Authenticate Redis
redis['password'] = 'redis-password-goes-here'
gitlab_rails['redis_password'] = 'redis-password-goes-here'
```
1. Run `reconfigure`:
@ -76,106 +55,7 @@ By default, GitLab uses UNIX sockets and is not set up to communicate via TCP. T
## Set up Sidekiq instance
1. SSH into the Sidekiq server.
1. Confirm that you can access the PostgreSQL, Gitaly, and Redis ports:
```shell
telnet <GitLab host> 5432 # PostgreSQL
telnet <GitLab host> 8075 # Gitaly
telnet <GitLab host> 6379 # Redis
```
1. [Download and install](https://about.gitlab.com/install/) the Linux package
using steps 1 and 2. **Do not complete any other steps.**
1. Copy the `/etc/gitlab/gitlab.rb` file from the GitLab instance and add the following settings. Make sure
to replace them with your values:
<!--
Updates to example must be made at:
- https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/sidekiq.md
- all reference architecture pages
-->
```ruby
# https://docs.gitlab.com/omnibus/roles/#sidekiq-roles
roles(["sidekiq_role"])
##
## To maintain uniformity of links across nodes, the
## `external_url` on the Sidekiq server should point to the external URL that users
## use to access GitLab. This can be either:
##
## - The `external_url` set on your application server.
## - The URL of a external load balancer, which routes traffic to the GitLab application server.
##
external_url 'https://gitlab.example.com'
# Configure the gitlab-shell API callback URL. Without this, `git push` will
# fail. This can be your 'front door' GitLab URL or an internal load
# balancer.
gitlab_rails['internal_api_url'] = 'GITLAB_URL'
gitlab_shell['secret_token'] = 'SHELL_TOKEN'
########################################
#### Redis ###
########################################
## Must be the same in every sentinel node.
redis['master_name'] = 'gitlab-redis' # Required if you have set up redis cluster
## The same password for Redis authentication you set up for the master node.
redis['master_password'] = '<redis_master_password>'
### If redis is running on the main Gitlab instance and you have opened the TCP port as above add the following
gitlab_rails['redis_host'] = '<gitlab_host>'
gitlab_rails['redis_port'] = 6379
#######################################
### Gitaly ###
#######################################
## Replace <gitaly_token> with the one you set up, see
## https://docs.gitlab.com/ee/administration/gitaly/configure_gitaly.html#about-the-gitaly-token
git_data_dirs({
"default" => {
"gitaly_address" => "tcp://<gitlab_host>:8075",
"gitaly_token" => "<gitaly_token>"
}
})
#######################################
### Postgres ###
#######################################
# Replace <database_host> and <database_password>
gitlab_rails['db_host'] = '<database_host>'
gitlab_rails['db_port'] = 5432
gitlab_rails['db_password'] = '<database_password>'
## Prevent database migrations from running on upgrade automatically
gitlab_rails['auto_migrate'] = false
#######################################
### Sidekiq configuration ###
#######################################
sidekiq['listen_address'] = "0.0.0.0"
## Set number of Sidekiq queue processes to the same number as available CPUs
sidekiq['queue_groups'] = ['*'] * 4
## Set number of Sidekiq threads per queue process to the recommend number of 20
sidekiq['max_concurrency'] = 20
```
1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the GitLab instance and replace the file in the Sidekiq instance.
1. Reconfigure GitLab:
```shell
sudo gitlab-ctl reconfigure
```
1. Restart the Sidekiq instance after completing the process and finishing the database migrations.
Find [your reference architecture](../reference_architectures/index.md#available-reference-architectures) and follow the Sidekiq instance setup details.
## Configure multiple Sidekiq nodes with shared storage

View File

@ -118,25 +118,6 @@ The rule for this job compares all files and paths in the current branch
recursively (`**/*`) against the `main` branch. The rule matches and the
job runs only when there are changes to the files in the branch.
### Use variables in `rules:changes`
You can use CI/CD variables in `rules:changes` expressions to determine when
to add jobs to a pipeline:
```yaml
docker build:
variables:
DOCKERFILES_DIR: 'path/to/files'
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- changes:
- $DOCKERFILES_DIR/**/*
```
You can use the `$` character for both variables and paths. For example, if the
`$DOCKERFILES_DIR` variable exists, its value is used. If it does not exist, the
`$` is interpreted as being part of a path.
## Common `if` clauses with predefined variables
`rules:if` clauses are commonly used with [predefined CI/CD variables](../variables/predefined_variables.md),

View File

@ -4104,8 +4104,7 @@ or [merge request pipelines](../pipelines/merge_request_pipelines.md), though
An array including any number of:
- Paths to files. The [file paths can include variables](../jobs/job_rules.md#use-variables-in-ruleschanges).
A file path array can also be in [`rules:changes:paths`](#ruleschangespaths).
- Paths to files. The file paths can include [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- Wildcard paths for:
- Single directories, for example `path/to/directory/*`.
- A directory and all its subdirectories, for example `path/to/directory/**/*`.
@ -4125,23 +4124,37 @@ docker build:
- Dockerfile
when: manual
allow_failure: true
docker build alternative:
variables:
DOCKERFILES_DIR: 'path/to/dockerfiles'
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- $DOCKERFILES_DIR/**/*
```
- If the pipeline is a merge request pipeline, check `Dockerfile` for changes.
In this example:
- If the pipeline is a merge request pipeline, check `Dockerfile` and the files in
`$DOCKERFILES_DIR/**/*` for changes.
- If `Dockerfile` has changed, add the job to the pipeline as a manual job, and the pipeline
continues running even if the job is not triggered (`allow_failure: true`).
- A maximum of 50 patterns or file paths can be defined per `rules:changes` section.
- If `Dockerfile` has not changed, do not add job to any pipeline (same as `when: never`).
- [`rules:changes:paths`](#ruleschangespaths) is the same as `rules:changes` without
any subkeys.
- If a file in `$DOCKERFILES_DIR/**/*` has changed, add the job to the pipeline.
- If no listed files have changed, do not add either job to any pipeline (same as `when: never`).
**Additional details**:
- Glob patterns are interpreted with Ruby's [`File.fnmatch`](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
with the [flags](https://docs.ruby-lang.org/en/master/File/Constants.html#module-File::Constants-label-Filename+Globbing+Constants+-28File-3A-3AFNM_-2A-29)
`File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`.
- A maximum of 50 patterns or file paths can be defined per `rules:changes` section.
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
- For additional examples, see [Specify when jobs run with `rules`](../jobs/job_rules.md).
- You can use the `$` character for both variables and paths. For example, if the
`$VAR` variable exists, its value is used. If it does not exist, the `$` is interpreted
as being part of a path.
**Related topics**:
@ -4161,7 +4174,7 @@ any subkeys. All additional details and related topics are the same.
**Possible inputs**:
- An array of file paths. [File paths can include variables](../jobs/job_rules.md#use-variables-in-ruleschanges).
- An array of file paths. File paths can include [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
**Example of `rules:changes:paths`**:
@ -4234,7 +4247,9 @@ Use `exists` to run a job when certain files exist in the repository.
**Possible inputs**:
- An array of file paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`) and can't directly link outside it. File paths can use glob patterns and [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- An array of file paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`)
and can't directly link outside it. File paths can use glob patterns and
[CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
**Example of `rules:exists`**:
@ -4246,10 +4261,12 @@ job:
- Dockerfile
job2:
variables:
DOCKERPATH: "**/Dockerfile"
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- exists:
- "**/Dockerfile"
- $DOCKERPATH
```
In this example:

View File

@ -1249,11 +1249,37 @@ include a visual representation to help readers understand it, you can:
The Markdown code for including an image in a document is:
`![Image description, used for alt tag](img/document_image_title_vX_Y.png)`
The image description is the alt text for the rendered image on the
documentation site. For accessibility and SEO, use [descriptions](https://webaim.org/techniques/alttext/)
that are accurate, succinct, and unique.
#### Alternative text
Don't use **image of** or **graphic of** to describe the image.
Alt text provides an accessible experience.
Screen readers use alt text to describe the image, and alt text displays
if an image fails to download.
Alt text should describe the context of the image, not the content. Add context that
relates to the topic of the page or section. Consider what you would say about the image
if you were helping someone read and interact with the page and they couldn't see it.
Do:
`![A runner sending a request to the Docker API.](img/document_image_title_vX_Y.png)`
Do not:
`![Runner and Docker architecture](img/document_image_title_vX_Y.png)`
When writing alt text:
- Write short, descriptive alt text in 155 characters or fewer. Screen readers
typically stop reading after this amount.
- If the image has complex information, like a workflow diagram, use a short alt text to identify the image and
include detailed information in the text.
- Use complete sentences.
- Use punctuation.
- Use sentence case and avoid using all-caps. Some screenreaders read capitals as individual letters.
- Don't use phrases like **Image of** or **Graphic of**.
- Don't use a string of keywords. Include keywords in a complete sentence to enhance context.
- Introduce the image in the section text, not the alt text.
- Try to avoid repeating content that you've already used in the section text.
#### Automatic screenshot generator

View File

@ -143,7 +143,7 @@ Service classes represent operations that coordinates changes between models
1. When there is no operation, there is no need to execute a service. The class would
probably be better designed as an entity, a value object, or a policy.
When implementing a service class, consider:
When implementing a service class, consider using the following patterns:
1. A service class initializer should contain in its arguments:
1. A [model](#models) instance that is being acted upon. Should be first positional
@ -187,9 +187,10 @@ When implementing a service class, consider:
end
```
1. It implements a single public instance method `#execute`, which invokes service class behavior:
1. The service class should implements a single public instance method `#execute`, which invokes service class behavior:
- The `#execute` method takes no arguments. All required data is passed into initializer.
- Optional. If needed, the `#execute` method returns its result via [`ServiceResponse`](#serviceresponse).
1. If a return value is needed, the `#execute` method should returns its result via [`ServiceResponse`](#serviceresponse) object.
Several base classes implement the service classes convention. You may consider inheriting from:
@ -197,6 +198,18 @@ Several base classes implement the service classes convention. You may consider
- `BaseProjectService` for services scoped to projects.
- `BaseGroupService` for services scoped to groups.
For some domains or [bounded contexts](software_design.md#bounded-contexts), it may make sense for
service classes to use different patterns. For example, the Remote Development domain uses a
[layered architecture](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#layered-architecture)
with domain logic isolated to a separate domain layer following a standard pattern, which allows for a very
[minimal service layer](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#minimal-service-layer)
which consists of only a single reusable `CommonService` class. It also uses
[functional patterns with stateless singleton class methods](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#functional-patterns).
See the Remote Development [service layer code example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#service-layer-code-example) for more details.
However, even though the invocation signature of services via this pattern is different,
it still respects the standard Service Class contract of always returning all results via a
[`ServiceResponse`](#serviceresponse) object.
Classes that are not service objects should be
[created elsewhere](software_design.md#use-namespaces-to-define-bounded-contexts),
such as in `lib`.

View File

@ -68,6 +68,31 @@ curl --verbose --user "$USER:$API_TOKEN" "https://$ATLASSIAN_SUBDOMAIN.atlassian
If the user can access the issue, Jira responds with a `200 OK` and the returned JSON includes the Jira issue details.
### Verify GitLab can post a comment to a Jira issue
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
To help troubleshoot your Jira integration, you can check whether
GitLab can post a comment to a Jira issue using the project's Jira
integration settings.
To do so:
- From a [Rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session),
run the following:
```ruby
jira_issue_id = "ALPHA-1" # Change to your Jira issue ID
project = Project.find_by_full_path("group/project") # Change to your project's path
integration = project.integrations.find_by(type: "Integrations::Jira")
jira_issue = integration.client.Issue.find(jira_issue_id)
jira_issue.comments.build.save!(body: 'This is a test comment from GitLab via the Rails console')
```
If the command is successful, a comment is added to the Jira issue.
## GitLab cannot create a Jira issue
When you try to create a Jira issue from a vulnerability, you might see a "field is required" error. For example, `Components is required` because a field called

View File

@ -356,7 +356,7 @@ If this is not the case, there are two options:
For a complete list of upgrade notices and special considerations for older versions, see the [Mattermost documentation](https://docs.mattermost.com/administration/important-upgrade-notes.html).
### GitLab Mattermost versions shipped with the Linux package
### GitLab Mattermost versions and edition shipped with the Linux package
Below is a list of Mattermost version changes for GitLab 15.0 and later:
@ -389,6 +389,9 @@ Below is a list of Mattermost version changes for GitLab 15.0 and later:
NOTE:
The Mattermost upgrade notes refer to different impacts when used with a PostgreSQL versus a MySQL database. The GitLab Mattermost included with the Linux package uses a PostgreSQL database.
The Linux package bundles the [Mattermost Team Edition](https://docs.mattermost.com/about/editions-and-offerings.html#mattermost-team-edition), which is a free and open source edition and does not include its commercial features.
To upgrade to the [Mattermost Enterprise Edition](https://docs.mattermost.com/about/editions-and-offerings.html#mattermost-enterprise-edition) see the Mattermost [documentation on upgrading](https://docs.mattermost.com/install/enterprise-install-upgrade.html#upgrading-to-enterprise-edition-in-gitlab-omnibus).
## OAuth 2.0 sequence diagram
The following image is a sequence diagram for how GitLab works as an OAuth 2.0

View File

@ -25,10 +25,11 @@ which users can make changes to your packages.
When a package is protected, the default behavior enforces these restrictions on the package:
| Action | Who can do it |
|:-------------------------|:----------------------------------------------------------------------------------|
| Protect a package | At least the Maintainer role. |
| Push a new package | At least the role set in [**Minimum access level for push**](#protect-a-package). |
| Action | Who can do it |
|:-----------------------------------------|:----------------------------------------------------------------------------------|
| Protect a package | At least the Maintainer role. |
| Push a new package | At least the role set in [**Minimum access level for push**](#protect-a-package). |
| Push a new package with a deploy token | Any user with a valid deploy token, even if a package is protected. |
## Protect a package

View File

@ -148,6 +148,7 @@ DETAILS:
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/430728) in GitLab 16.11 [with a flag](../../../../administration/feature_flags.md) named `mr_reviewer_requests_changes`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/451211) in GitLab 17.2.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it

13
lib/gitlab/fp/README.md Normal file
View File

@ -0,0 +1,13 @@
# Functional Programming in Ruby
## Overview
The `Gitlab::Fp` module contains support for functional programming patterns ("FP") in Ruby.
It is in the process of being extracted from the `RemoteDevelopment` domain code,
and made available for wider use in the monolith.
See the following sections in the [Remote Development Rails domain developer documentation](../../../ee/lib/remote_development/README.md) for more context:
- ["Functional Programming"](../../../ee/lib/remote_development/README.md#functional-patterns)
- ["Railway Oriented Programming and the Result Class"](../../../ee/lib/remote_development/README.md#railway-oriented-programming-and-the-result-class)

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
module Gitlab
# noinspection RubyClassModuleNamingConvention -- JetBrains is changing this to allow shorter names
module Fp
module RopHelpers
# @param [Class] base
# @return void
def self.extended(base)
base.class_eval do
private_class_method :retrieve_single_public_singleton_method, :public_singleton_methods_to_ignore
end
end
# @param [Class] fp_module_or_class
# @raise [RuntimeError]
# @return [Symbol]
def retrieve_single_public_singleton_method(fp_module_or_class)
fp_class_singleton_methods = fp_module_or_class.singleton_methods(false)
public_singleton_methods = fp_class_singleton_methods - public_singleton_methods_to_ignore
return public_singleton_methods.first if public_singleton_methods.size == 1
fp_doc_link =
"https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#functional-patterns"
rop_doc_link =
"https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#railway-oriented-programming-and-the-result-class"
if public_singleton_methods.size > 1
err_msg =
"Railway Oriented Programming (ROP) pattern violation in class `#{fp_module_or_class}`. " \
"Expected exactly one (1) public entry point singleton/class method to be present " \
"in a class which is used with the ROP pattern, but " \
"#{public_singleton_methods.size} " \
"public singleton methods were found: #{public_singleton_methods.sort.join(', ')}. " \
"You can make the non-entry-point method(s) private via `private_class_method :method_name`. " \
"See #{fp_doc_link} and #{rop_doc_link} for more information."
raise(ArgumentError, err_msg)
end
err_msg =
"Railway Oriented Programming (ROP) pattern violation in class `#{fp_module_or_class}`. " \
"Expected exactly one public entry point singleton/class method to be present " \
"in a class which is used with the ROP pattern, but " \
"no public singleton methods were found. " \
"See #{fp_doc_link} and #{rop_doc_link} for more information."
raise(ArgumentError, err_msg)
end
# @return [Array<Symbol>]
def public_singleton_methods_to_ignore
# Singleton methods added by other libraries that we need to ignore.
Module.singleton_methods(false) + Class.singleton_methods(false)
end
end
end
end

View File

@ -4,6 +4,14 @@ require 'active_model/errors'
module RemoteDevelopment
module MessageSupport
# @param [Class] base
# @return void
def self.extended(base)
base.class_eval do
private_class_method :generate_error_response_from_message
end
end
# @param [RemoteDevelopment::Message] message
# @param [Symbol] reason
# @return [Hash]

View File

@ -6,8 +6,6 @@ module RemoteDevelopment
include Messages
extend MessageSupport
private_class_method :generate_error_response_from_message
# @param [Hash] context
# @return [Hash]
# @raise [UnmatchedResultError]

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 14', '>= 14.10.0', require: 'gitlab/qa'
gem 'gitlab-qa', '~> 14', '>= 14.11.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 1.29.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8.4' # This should stay in sync with the root's Gemfile

View File

@ -131,7 +131,7 @@ GEM
gitlab (4.19.0)
httparty (~> 0.20)
terminal-table (>= 1.5.1)
gitlab-qa (14.10.0)
gitlab-qa (14.11.0)
activesupport (>= 6.1, < 7.2)
gitlab (~> 4.19)
http (~> 5.0)
@ -392,7 +392,7 @@ DEPENDENCIES
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-cng!
gitlab-qa (~> 14, >= 14.10.0)
gitlab-qa (~> 14, >= 14.11.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 1.29.0)
influxdb-client (~> 3.1)
@ -418,4 +418,4 @@ DEPENDENCIES
zeitwerk (~> 2.6, >= 2.6.15)
BUNDLED WITH
2.5.13
2.5.14

View File

@ -7,6 +7,7 @@ module QA
class Index < QA::Page::Base
view 'app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue' do
element 'select-all-artifacts-checkbox'
element 'job-artifact-table-row'
end
view 'app/assets/javascripts/ci/artifacts/components/artifacts_bulk_delete.vue' do
@ -17,11 +18,6 @@ module QA
element 'artifacts-bulk-delete-modal'
end
view 'app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue' do
element 'job-artifacts-count'
element 'job-artifacts-size'
end
def select_all
check_element('select-all-artifacts-checkbox', true)
end
@ -34,12 +30,8 @@ module QA
end
end
def job_artifacts_count_by_row(row: 1)
all_elements('job-artifacts-count', minimum: row)[row - 1].text.gsub(/[^0-9]/, '').to_i
end
def job_artifacts_size_by_row(row: 1)
all_elements('job-artifacts-size', minimum: row)[row - 1].text.gsub(/[^0-9]/, '').to_f
def has_no_artifacts?
has_no_element?('job-artifact-table-row')
end
end
end

View File

@ -31,14 +31,8 @@ module QA
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/425725' do
Page::Project::Artifacts::Index.perform do |index|
index.delete_selected_artifacts
position = rand(1..20)
artifacts_count = index.job_artifacts_count_by_row(row: position)
artifacts_size = index.job_artifacts_size_by_row(row: position)
aggregate_failures 'job artifacts count and size' do
expect(artifacts_count).to eq(0), 'Failed to delete artifact'
expect(artifacts_size).to eq(0), 'Failed to delete artifact'
end
expect(index).to have_no_artifacts
end
end
end

View File

@ -189,7 +189,15 @@ module QA
element.to_s.underline.bright
end
# Log message for has_element? and has_no_element? methods
#
# @param [String] method the method name
# @param [Symbol, String, QA::Page::Element] name the name of the element
# @param [Boolean] found the result of the method
# @param [Hash] kwargs
def log_has_element_or_not(method, name, found, **kwargs)
name = name.name if name.is_a? QA::Page::Element
msg = ["#{method} :#{name}"]
msg << %(with text "#{kwargs[:text]}") if kwargs[:text]
msg << "class: #{kwargs[:class]}" if kwargs[:class]

View File

@ -30,7 +30,8 @@ function run_rubocop {
while IFS= read -r -d '' file; do
files_for_rubocop+=("$file")
done < <(find . -path './**/remote_development/*.rb' -print0)
files_for_rails+=(
files_for_rubocop+=(
"lib/gitlab/fp/rop_helpers.rb"
"lib/result.rb"
"spec/lib/result_spec.rb"
"spec/support/matchers/invoke_rop_steps.rb"
@ -49,7 +50,7 @@ function run_rspec_fast {
files_for_fast=()
while IFS= read -r file; do
files_for_fast+=("$file")
done < <(find ee/spec -path '**/remote_development/*_spec.rb' -exec grep -lE 'require_relative.*rd_fast_spec_helper' {} +)
done < <(find spec ee/spec -path '**/remote_development/*_spec.rb' -exec grep -lE 'require_relative.*rd_fast_spec_helper' {} +)
bin/rspec "${files_for_fast[@]}"
}
@ -64,13 +65,14 @@ function run_rspec_rails {
files_for_rails=()
while IFS= read -r file; do
files_for_rails+=("$file")
done < <(find ee/spec -path '**/remote_development/*_spec.rb' | grep -v 'qa/qa' | grep -v '/features/')
done < <(find spec ee/spec -path '**/remote_development/*_spec.rb' | grep -v 'qa/qa' | grep -v '/features/')
files_for_rails+=(
"ee/spec/graphql/types/query_type_spec.rb"
"ee/spec/graphql/types/subscription_type_spec.rb"
"ee/spec/requests/api/internal/kubernetes_spec.rb"
"spec/graphql/types/subscription_type_spec.rb"
"spec/lib/gitlab/fp/rop_helpers_spec.rb"
"spec/lib/result_spec.rb"
"spec/support_specs/matchers/result_matchers_spec.rb"
)

View File

@ -118,6 +118,24 @@ describe('JobArtifactsTable component', () => {
};
const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
const emptyJob = {
...job,
artifacts: { nodes: [] },
};
const getJobArtifactsResponseWithEmptyJob = {
data: {
...getJobArtifactsResponse.data,
project: {
...getJobArtifactsResponse.data.project,
jobs: {
nodes: [emptyJob],
pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo },
},
},
},
};
const archiveArtifact = job.artifacts.nodes.find(
(artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
);
@ -810,6 +828,47 @@ describe('JobArtifactsTable component', () => {
});
});
describe('refetch behavior', () => {
describe('without no empty jobs', () => {
const query = jest.fn().mockResolvedValue(getJobArtifactsResponse);
beforeEach(async () => {
createComponent({
handlers: {
getJobArtifactsQuery: query,
},
});
await waitForPromises();
});
it('only fetches artifacts once', () => {
expect(query).toHaveBeenCalledTimes(1);
});
});
describe('with an empty job', () => {
const query = jest
.fn()
.mockResolvedValueOnce(getJobArtifactsResponseWithEmptyJob)
.mockResolvedValue(getJobArtifactsResponse);
beforeEach(async () => {
createComponent({
handlers: {
getJobArtifactsQuery: query,
},
});
await waitForPromises();
});
it('refetches to clear empty jobs', () => {
expect(query).toHaveBeenCalledTimes(2);
});
});
});
describe('pagination', () => {
const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs;
const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates);

View File

@ -1,8 +1,16 @@
import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils';
import {
totalArtifactsSizeForJob,
mapArchivesToJobNodes,
mapBooleansToJobNodes,
} from '~/ci/artifacts/utils';
const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
const emptyJob = {
...job,
artifacts: { nodes: [] },
};
const artifacts = job.artifacts.nodes;
describe('totalArtifactsSizeForJob', () => {
@ -14,3 +22,21 @@ describe('totalArtifactsSizeForJob', () => {
);
});
});
describe('mapArchivesToJobNodes', () => {
it('sets archive to the archive artifact for each job node', () => {
expect([job, emptyJob].map(mapArchivesToJobNodes)).toMatchObject([
{ archive: { name: 'ci_build_artifacts.zip' } },
{ archive: {} },
]);
});
});
describe('mapBooleansToJobNodes', () => {
it('sets hasArtifacts and hasMetadata for each job node', () => {
expect([job, emptyJob].map(mapBooleansToJobNodes)).toMatchObject([
{ hasArtifacts: true, hasMetadata: true },
{ hasArtifacts: false, hasMetadata: false },
]);
});
});

View File

@ -365,6 +365,7 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: 'ID_DESC',
});
});
});
@ -426,6 +427,7 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: 'ID_DESC',
});
});
@ -439,6 +441,7 @@ describe('Pipeline schedules app', () => {
last: null,
prevPageCursor: '',
nextPageCursor: pageInfo.endCursor,
sortValue: 'ID_DESC',
});
expect(findPagination().props('value')).toEqual(2);
});
@ -456,6 +459,50 @@ describe('Pipeline schedules app', () => {
});
});
describe('when sorting changes', () => {
const newSort = 'DESCRIPTION_ASC';
beforeEach(async () => {
createComponent([[getPipelineSchedulesQuery, successHandler]]);
await waitForPromises();
await findTable().vm.$emit('update-sorting', newSort, 'description', false);
});
it('passes it to the graphql query', () => {
expect(successHandler).toHaveBeenCalledTimes(2);
expect(successHandler.mock.calls[1][0]).toEqual({
projectPath: 'gitlab-org/gitlab',
ids: null,
first: SCHEDULES_PER_PAGE,
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: newSort,
});
});
});
describe('when update-sorting event is emitted', () => {
beforeEach(async () => {
createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
await waitForPromises();
});
it('resets the page count', async () => {
expect(findPagination().props('value')).toEqual(1);
await setPage(2);
expect(findPagination().props('value')).toEqual(2);
await findTable().vm.$emit('update-sorting', 'DESCRIPTION_DESC', 'description', true);
await waitForPromises();
expect(findPagination().props('value')).toEqual(1);
});
});
it.each`
description | handler | buttonDisabled | alertExists
${'limit reached'} | ${planLimitReachedHandler} | ${true} | ${true}

View File

@ -0,0 +1,89 @@
import { GlTable } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import {
TH_DESCRIPTION_TEST_ID,
TH_TARGET_TEST_ID,
TH_NEXT_TEST_ID,
} from '~/ci/pipeline_schedules/constants';
import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../mock_data';
describe('Pipeline schedules table', () => {
let wrapper;
const createComponent = () => {
wrapper = mountExtended(PipelineSchedulesTable, {
propsData: {
schedules: mockPipelineScheduleNodes,
currentUser: mockPipelineScheduleCurrentUser,
sortBy: 'ID',
sortDesc: true,
},
});
};
const findTable = () => wrapper.findComponent(GlTable);
describe('sorting', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it.each`
sortValue | sortBy | sortDesc
${'DESCRIPTION_ASC'} | ${'description'} | ${false}
${'DESCRIPTION_DESC'} | ${'description'} | ${true}
${'REF_ASC'} | ${'target'} | ${false}
${'REF_DESC'} | ${'target'} | ${true}
${'NEXT_RUN_AT_ASC'} | ${'next'} | ${false}
${'NEXT_RUN_AT_DESC'} | ${'next'} | ${true}
`(
'emits sort data in expected format for sortValue $sortValue',
({ sortValue, sortBy, sortDesc }) => {
findTable().vm.$emit('sort-changed', { sortBy, sortDesc });
expect(wrapper.emitted('update-sorting')[0]).toEqual([sortValue, sortBy, sortDesc]);
},
);
it('emits no update-sorting event when called with unsortable column', () => {
findTable().vm.$emit('sort-changed', { sortBy: 'actions', sortDesc: false });
expect(wrapper.emitted('update-sorting')).toBeUndefined();
});
it('emits no update-sorting event when called with unknown column', () => {
findTable().vm.$emit('sort-changed', { sortBy: 'not-defined-never', sortDesc: false });
expect(wrapper.emitted('update-sorting')).toBeUndefined();
});
});
describe('sorting the pipeline schedules table by column', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it.each`
description | selector
${'description'} | ${TH_DESCRIPTION_TEST_ID}
${'target'} | ${TH_TARGET_TEST_ID}
${'next'} | ${TH_NEXT_TEST_ID}
`('updates sort with new direction when sorting by $description', async ({ selector }) => {
const [[attr, value]] = Object.entries(selector);
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
expect(columnHeader().attributes('aria-sort')).toBe('none');
columnHeader().trigger('click');
await waitForPromises();
expect(columnHeader().attributes('aria-sort')).toBe('ascending');
columnHeader().trigger('click');
await waitForPromises();
expect(columnHeader().attributes('aria-sort')).toBe('descending');
});
});
});

View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Fp::RopHelpers, feature_category: :shared do
describe '.retrieve_single_public_singleton_method' do
let(:extending_class) do
Class.new do
extend Gitlab::Fp::RopHelpers
def self.execute(class_object)
retrieve_single_public_singleton_method(class_object)
end
end
end
let(:doc_link) do
"https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#functional-patterns " \
"and https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#railway-oriented-programming-and-the-result-class"
end
before do
stub_const(class_or_module_name, class_or_module)
end
context "when there is exactly one public singleton method" do
shared_examples "a class or module with a single public singleton method" do
it "returns the single public singleton method", :unlimited_max_formatted_output_length do
expect(extending_class.execute(class_or_module)).to eq(:public_method_one)
end
end
let(:class_or_module_name) { 'ClassWithOnePublicSingletonMethod' }
let(:expected_error_message_pattern) do
/violation.*`#{class_or_module_name}`.*2.*found.*public_method_one, public_method_two.*private.*#{doc_link}/
end
context "for a class" do
let(:class_or_module) do
Class.new do
def self.public_method_one
puts 'no-op'
end
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module with a single public singleton method"
end
context "for a module" do
let(:class_or_module) do
Module.new do
def self.public_method_one
puts 'no-op'
end
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module with a single public singleton method"
end
end
context "for invalid arguments" do
shared_examples "a class or module without a single public singleton method" do
it "raises an error", :unlimited_max_formatted_output_length do
expect { extending_class.execute(class_or_module) }
.to raise_error(ArgumentError, expected_error_message_pattern)
end
end
context "when there is more than one public singleton method" do
let(:class_or_module_name) { 'ClassWithMultiplePublicSingletonMethods' }
let(:expected_error_message_pattern) do
/violation.*`#{class_or_module_name}`.*2.*found.*public_method_one, public_method_two.*private.*#{doc_link}/
end
context "for a class" do
let(:class_or_module) do
Class.new do
def self.public_method_one
puts 'no-op'
end
def self.public_method_two
puts 'no-op'
end
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module without a single public singleton method"
end
context "for a module" do
let(:class_or_module) do
Module.new do
def self.public_method_one
puts 'no-op'
end
def self.public_method_two
puts 'no-op'
end
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module without a single public singleton method"
end
end
context "when there are no public singleton methods" do
let(:class_or_module_name) { 'ClassWithNoPublicSingletonMethods' }
let(:expected_error_message_pattern) do
/violation.*`#{class_or_module_name}`.*no public singleton methods were found.*#{doc_link}/
end
context "for a class" do
let(:class_or_module) do
Class.new do
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module without a single public singleton method"
end
context "for a module" do
let(:class_or_module) do
Module.new do
def self.private_method
puts 'no-op'
end
private_class_method :private_method
end
end
it_behaves_like "a class or module without a single public singleton method"
end
end
end
end
end

View File

@ -3,6 +3,18 @@
require_relative 'rd_fast_spec_helper'
RSpec.describe RemoteDevelopment::MessageSupport, :rd_fast, feature_category: :remote_development do
let(:extending_class) do
Class.new do
extend RemoteDevelopment::MessageSupport
# @param [RemoteDevelopment::Message] message
# @return [Hash]
def self.execute(message)
generate_error_response_from_message(message: message, reason: :does_not_matter)
end
end
end
let(:object) { Object.new.extend(described_class) }
describe '.generate_error_response_from_message' do
@ -10,7 +22,7 @@ RSpec.describe RemoteDevelopment::MessageSupport, :rd_fast, feature_category: :r
let(:message) { RemoteDevelopment::Message.new(content: { unsupported: 'unmatched' }) }
it 'raises an error' do
expect { object.generate_error_response_from_message(message: message, reason: :does_not_matter) }
expect { extending_class.execute(message) }
.to raise_error(/Unexpected message content/)
end
end

View File

@ -8,7 +8,7 @@ require 'fast_spec_helper'
#
# To support this, we have intentionally used some `rubocop:disable` comments to allow for more
# explicit and readable examples.
# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration -- intentionally disabled per comment above
RSpec.describe Result, feature_category: :remote_development do
describe 'usage of Result.ok and Result.err' do
context 'when checked with .ok? and .err?' do

View File

@ -429,6 +429,28 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
it_behaves_like params[:shared_examples_name]
end
context 'with deploy token' do
let_it_be(:deploy_token) { create(:deploy_token, projects: [project]) }
let_it_be(:current_user) { deploy_token }
let_it_be(:user) { nil }
where(:package_name_pattern, :minimum_access_level_for_push, :shared_examples_name) do
ref(:package_name) | :maintainer | 'valid package'
ref(:package_name) | :owner | 'valid package'
ref(:package_name) | :admin | 'valid package'
ref(:package_name_pattern_no_match) | :owner | 'valid package'
end
with_them do
before do
package_protection_rule.update!(package_name_pattern: package_name_pattern,
minimum_access_level_for_push: minimum_access_level_for_push)
end
it_behaves_like params[:shared_examples_name]
end
end
end
describe '#lease_key' do

View File

@ -1,12 +1,11 @@
# frozen_string_literal: true
require_relative '../../../lib/gitlab/fp/rop_helpers'
module InvokeRopSteps
private
def public_methods_to_ignore
# Singleton methods to exist on class objects by default that we need to ignore.
[:yaml_tag, :method]
end
include Gitlab::Fp::RopHelpers
def add_err_result_for_step(err_result_for_step, err_results_for_steps)
result_type = :err
@ -117,7 +116,7 @@ module InvokeRopSteps
step_action = rop_step[1]
expected_rop_step = {
step_class: step_class,
step_class_method: retrieve_rop_class_method(step_class),
step_class_method: retrieve_single_public_singleton_method(step_class),
step_action: step_action
}
@ -144,16 +143,6 @@ module InvokeRopSteps
expected_rop_steps
end
def retrieve_rop_class_method(step_class)
public_methods = step_class.singleton_methods(false).reject { |method| public_methods_to_ignore.include?(method) }
if public_methods.size != 1
raise "Pattern violation in class #{step_class}: exactly one public method must be present in an ROP class"
end
public_methods.first
end
def setup_mock_expectations_for_steps(steps:, context_passed_along_steps:)
steps.each do |step|
step => {
@ -218,9 +207,9 @@ RSpec::Matchers.define :invoke_rop_steps do |rop_steps|
end
chain :from_main_class do |clazz|
validate_main_class(clazz)
main_class = clazz
main_class_method = retrieve_rop_class_method(main_class)
validate_main_class(main_class)
main_class_method = retrieve_single_public_singleton_method(main_class)
expect(main_class).to receive(main_class_method).and_call_original
end