diff options
101 files changed, 1046 insertions, 940 deletions
@@ -409,7 +409,7 @@ group :development, :test do end group :development, :test, :danger do - gem 'gitlab-dangerfiles', '~> 3.4.2', require: false + gem 'gitlab-dangerfiles', '~> 3.4.3', require: false end group :development, :test, :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index c0bedd9bb40..16ffc6e8c45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -501,7 +501,7 @@ GEM terminal-table (~> 1.5, >= 1.5.1) gitlab-chronic (0.10.5) numerizer (~> 0.2) - gitlab-dangerfiles (3.4.2) + gitlab-dangerfiles (3.4.3) danger (>= 8.4.5) danger-gitlab (>= 8.0.0) rake @@ -1560,7 +1560,7 @@ DEPENDENCIES gitaly (~> 15.1.0.pre.rc1) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) - gitlab-dangerfiles (~> 3.4.2) + gitlab-dangerfiles (~> 3.4.3) gitlab-experiment (~> 0.7.1) gitlab-fog-azure-rm (~> 1.3.0) gitlab-labkit (~> 0.23.0) diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js index 2a4916ecabf..b8ac17a01f2 100644 --- a/app/assets/javascripts/projects/project_visibility.js +++ b/app/assets/javascripts/projects/project_visibility.js @@ -24,7 +24,7 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) { optionInput.disabled = true; const reason = option.querySelector('.option-disabled-reason'); if (reason) { - const optionTitle = option.querySelector('.form-check-label span'); + const optionTitle = option.querySelector('.js-visibility-level-radio span'); const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : ''; reason.innerHTML = sprintf( __( diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 41f7a4b147f..1f6b5e98122 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -103,14 +103,12 @@ export default { return this.rowNumbers[key]; }, - getCommit(fileName, type) { + getCommit(fileName) { if (!this.glFeatures.lazyLoadCommits) { return {}; } - return this.commits.find( - (commitEntry) => commitEntry.fileName === fileName && commitEntry.type === type, - ); + return this.commits.find((commitEntry) => commitEntry.fileName === fileName); }, }, }; @@ -152,7 +150,7 @@ export default { :loading-path="loadingPath" :total-entries="totalEntries" :row-number="generateRowNumber(entry.flatPath, entry.id, index)" - :commit-info="getCommit(entry.name, entry.type)" + :commit-info="getCommit(entry.name)" v-on="$listeners" /> </template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 2b910109f7d..4a823738ea1 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -43,7 +43,6 @@ export default { variables() { return { fileName: this.name, - type: this.type, path: this.currentPath, projectPath: this.projectPath, maxOffset: this.totalEntries, diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index 29aabe1b00f..3a59a02af01 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -9,7 +9,7 @@ Vue.use(VueApollo); const defaultClient = createDefaultClient( { Query: { - commit(_, { path, fileName, type, maxOffset }) { + commit(_, { path, fileName, maxOffset }) { return new Promise((resolve) => { fetchLogsTree( defaultClient, @@ -19,7 +19,6 @@ const defaultClient = createDefaultClient( resolve, entry: { name: fileName, - type, }, }, maxOffset, diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index ac02392d60f..9345a8406e3 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -16,9 +16,7 @@ function setNextOffset(offset) { } export function resolveCommit(commits, path, { resolve, entry }) { - const commit = commits.find( - (c) => c.filePath === `${path}/${entry.name}` && c.type === entry.type, - ); + const commit = commits.find((c) => c.filePath === `${path}/${entry.name}`); if (commit) { resolve(commit); diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql index b046fc1f730..80dedfe3e3f 100644 --- a/app/assets/javascripts/repository/queries/commit.fragment.graphql +++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql @@ -6,5 +6,4 @@ fragment TreeEntryCommit on LogTreeCommit { commitPath fileName filePath - type } diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql index 7ae4a3b984a..1a01462bd19 100644 --- a/app/assets/javascripts/repository/queries/commit.query.graphql +++ b/app/assets/javascripts/repository/queries/commit.query.graphql @@ -1,7 +1,7 @@ #import "ee_else_ce/repository/queries/commit.fragment.graphql" -query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) { - commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client { +query getCommit($fileName: String!, $path: String!, $maxOffset: Number!) { + commit(path: $path, fileName: $fileName, maxOffset: $maxOffset) @client { ...TreeEntryCommit } } diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js index a67252ec004..878b4fdd71a 100644 --- a/app/assets/javascripts/repository/utils/commit.js +++ b/app/assets/javascripts/repository/utils/commit.js @@ -7,7 +7,6 @@ export function normalizeData(data, path, extra = () => {}) { commitPath: d.commit_path, fileName: d.file_name, filePath: `${path}/${d.file_name}`, - type: d.type, __typename: 'LogTreeCommit', ...extra(d), })); diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue index 06a8eb790fc..9fa4b521ebc 100644 --- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue +++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui'; +import { GlBadge, GlTabs, GlTab, GlTooltipDirective } from '@gitlab/ui'; import { createAlert, VARIANT_SUCCESS } from '~/flash'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; @@ -11,7 +11,7 @@ import RunnerPauseButton from '../components/runner_pause_button.vue'; import RunnerHeader from '../components/runner_header.vue'; import RunnerDetails from '../components/runner_details.vue'; import RunnerJobs from '../components/runner_jobs.vue'; -import { I18N_FETCH_ERROR } from '../constants'; +import { I18N_DETAILS, I18N_FETCH_ERROR } from '../constants'; import runnerQuery from '../graphql/show/runner.query.graphql'; import { captureException } from '../sentry_utils'; import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; @@ -20,6 +20,7 @@ export default { name: 'AdminRunnerShowApp', components: { GlBadge, + GlTabs, GlTab, RunnerDeleteButton, RunnerEditButton, @@ -84,6 +85,7 @@ export default { redirectTo(this.runnersPath); }, }, + I18N_DETAILS, }; </script> <template> @@ -96,24 +98,27 @@ export default { </template> </runner-header> - <runner-details :runner="runner"> - <template #jobs-tab> - <gl-tab> - <template #title> - {{ s__('Runners|Jobs') }} - <gl-badge - v-if="jobCount" - data-testid="job-count-badge" - class="gl-tab-counter-badge" - size="sm" - > - {{ jobCount }} - </gl-badge> - </template> + <gl-tabs> + <gl-tab> + <template #title>{{ $options.I18N_DETAILS }}</template> - <runner-jobs v-if="runner" :runner="runner" /> - </gl-tab> - </template> - </runner-details> + <runner-details v-if="runner" :runner="runner" /> + </gl-tab> + <gl-tab> + <template #title> + {{ s__('Runners|Jobs') }} + <gl-badge + v-if="jobCount" + data-testid="job-count-badge" + class="gl-tab-counter-badge" + size="sm" + > + {{ jobCount }} + </gl-badge> + </template> + + <runner-jobs v-if="runner" :runner="runner" /> + </gl-tab> + </gl-tabs> </div> </template> diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index 3c03804a79f..60469d26dd5 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -1,5 +1,5 @@ <script> -import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui'; +import { GlIntersperse } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; @@ -11,8 +11,6 @@ import RunnerTags from './runner_tags.vue'; export default { components: { - GlTabs, - GlTab, GlIntersperse, RunnerDetail, RunnerMaintenanceNoteDetail: () => @@ -65,64 +63,57 @@ export default { </script> <template> - <gl-tabs> - <gl-tab> - <template #title>{{ s__('Runners|Details') }}</template> - - <template v-if="runner"> - <runner-upgrade-status-alert class="gl-my-4" :runner="runner" /> - <div class="gl-pt-4"> - <dl class="gl-mb-0" data-testid="runner-details-list"> - <runner-detail :label="s__('Runners|Description')" :value="runner.description" /> - <runner-detail - :label="s__('Runners|Last contact')" - :empty-value="s__('Runners|Never contacted')" - > - <template #value> - <time-ago v-if="runner.contactedAt" :time="runner.contactedAt" /> - </template> - </runner-detail> - <runner-detail :label="s__('Runners|Version')"> - <template v-if="runner.version" #value> - {{ runner.version }} - <runner-upgrade-status-badge size="sm" :runner="runner" /> - </template> - </runner-detail> - <runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" /> - <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" /> - <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" /> - <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" /> - <runner-detail :label="s__('Runners|Configuration')"> - <template #value> - <gl-intersperse v-if="configTextProtected || configTextUntagged"> - <span v-if="configTextProtected">{{ configTextProtected }}</span> - <span v-if="configTextUntagged">{{ configTextUntagged }}</span> - </gl-intersperse> - </template> - </runner-detail> - <runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> - <runner-detail :label="s__('Runners|Tags')"> - <template #value> - <runner-tags - v-if="runner.tagList && runner.tagList.length" - class="gl-vertical-align-middle" - :tag-list="runner.tagList" - size="sm" - /> - </template> - </runner-detail> - - <runner-maintenance-note-detail - class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid" - :value="runner.maintenanceNoteHtml" + <div> + <runner-upgrade-status-alert class="gl-my-4" :runner="runner" /> + <div class="gl-pt-4"> + <dl class="gl-mb-0" data-testid="runner-details-list"> + <runner-detail :label="s__('Runners|Description')" :value="runner.description" /> + <runner-detail + :label="s__('Runners|Last contact')" + :empty-value="s__('Runners|Never contacted')" + > + <template #value> + <time-ago v-if="runner.contactedAt" :time="runner.contactedAt" /> + </template> + </runner-detail> + <runner-detail :label="s__('Runners|Version')"> + <template v-if="runner.version" #value> + {{ runner.version }} + <runner-upgrade-status-badge size="sm" :runner="runner" /> + </template> + </runner-detail> + <runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" /> + <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" /> + <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" /> + <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" /> + <runner-detail :label="s__('Runners|Configuration')"> + <template #value> + <gl-intersperse v-if="configTextProtected || configTextUntagged"> + <span v-if="configTextProtected">{{ configTextProtected }}</span> + <span v-if="configTextUntagged">{{ configTextUntagged }}</span> + </gl-intersperse> + </template> + </runner-detail> + <runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> + <runner-detail :label="s__('Runners|Tags')"> + <template #value> + <runner-tags + v-if="runner.tagList && runner.tagList.length" + class="gl-vertical-align-middle" + :tag-list="runner.tagList" + size="sm" /> - </dl> - </div> + </template> + </runner-detail> + + <runner-maintenance-note-detail + class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid" + :value="runner.maintenanceNoteHtml" + /> + </dl> + </div> - <runner-groups v-if="isGroupRunner" :runner="runner" /> - <runner-projects v-if="isProjectRunner" :runner="runner" /> - </template> - </gl-tab> - <slot name="jobs-tab"></slot> - </gl-tabs> + <runner-groups v-if="isGroupRunner" :runner="runner" /> + <runner-projects v-if="isProjectRunner" :runner="runner" /> + </div> </template> diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index b9621c26b59..64541729701 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -81,6 +81,7 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__( // Runner details +export const I18N_DETAILS = s__('Runners|Details'); export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})'); export const I18N_NONE = __('None'); export const I18N_NO_JOBS_FOUND = s__('Runners|This runner has not run any jobs.'); diff --git a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue index ce5033d23f3..75138b1bd81 100644 --- a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue +++ b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue @@ -89,6 +89,6 @@ export default { </template> </runner-header> - <runner-details :runner="runner" /> + <runner-details v-if="runner" :runner="runner" /> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 3ed961bcc1f..410331004e4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -315,6 +315,7 @@ export default { @mouseup="onRowMouseUp" > <status-icon + :level="1" :name="$options.label || $options.name" :is-loading="isLoadingSummary" :icon-name="statusIconName" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue index bb626c9adba..25b7e652657 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue @@ -9,6 +9,11 @@ export default { GlIcon, }, props: { + level: { + type: Number, + required: false, + default: 0, + }, name: { type: String, required: false, @@ -27,7 +32,7 @@ export default { size: { type: Number, required: false, - default: 16, + default: 12, }, }, computed: { @@ -44,8 +49,8 @@ export default { <div :class="[ $options.EXTENSION_ICON_CLASS[iconName], - { 'mr-widget-extension-icon': !isLoading && size === 16 }, - { 'gl-p-2': isLoading || size === 16 }, + { 'mr-widget-extension-icon': !isLoading && level === 1 }, + { 'gl-p-2': isLoading || level === 1 }, ]" class="gl-rounded-full gl-mr-3 gl-relative gl-p-2" > diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js index a7aaa2f4476..ca95e1b5de8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -32,7 +32,7 @@ export default { // Status icon to be used next to the summary text // Receives the collapsed data as an argument statusIcon(count) { - return EXTENSION_ICONS.warning; + return EXTENSION_ICONS.failed; }, // Tertiary action buttons that will take the user elsewhere // in the GitLab app diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 8d596465970..dc5c88b1eb3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -8,6 +8,10 @@ import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_optio import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '../vue_shared/translate'; +import { registerExtension } from './components/extensions'; +import issuesExtension from './extensions/issues'; + +registerExtension(issuesExtension); Vue.use(Translate); Vue.use(VueApollo); diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index 2c1e15cf8b7..1b27e51e793 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -630,6 +630,24 @@ $tabs-holder-z-index: 250; height: 24px; } +.mr-widget-extension-icon::after { + @include gl-content-empty; + @include gl-absolute; + @include gl-rounded-full; + + top: 4px; + left: 4px; + width: 16px; + height: 16px; + border: 4px solid currentColor; +} + +.mr-widget-extension-icon svg { + position: relative; + top: 2px; + left: 2px; +} + .mr-widget-heading { position: relative; border: 1px solid var(--border-color, $border-color); diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 55178a1a445..bb16c2d2098 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -221,7 +221,7 @@ class RegistrationsController < Devise::RegistrationsController return unless member - Gitlab::Tracking.event(self.class.name, 'accepted', label: 'invite_email', property: member.id.to_s) + Gitlab::Tracking.event(self.class.name, 'accepted', label: 'invite_email', property: member.id.to_s, user: resource) end def context_user diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index 43dc0c4ce85..d906c577aa5 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -40,6 +40,8 @@ module Types authorize: :download_code field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?, description: 'Indicates the release is an upcoming release.' + field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?, + description: 'Indicates the release is an historical release.' field :author, Types::UserType, null: true, description: 'User that created the release.' diff --git a/app/graphql/types/work_items/widgets/assignees_type.rb b/app/graphql/types/work_items/widgets/assignees_type.rb index 001ace77d6e..08ee06fdfa0 100644 --- a/app/graphql/types/work_items/widgets/assignees_type.rb +++ b/app/graphql/types/work_items/widgets/assignees_type.rb @@ -17,6 +17,13 @@ module Types field :allows_multiple_assignees, GraphQL::Types::Boolean, null: true, method: :allows_multiple_assignees?, description: 'Indicates whether multiple assignees are allowed.' + + field :can_invite_members, GraphQL::Types::Boolean, null: false, resolver_method: :can_invite_members?, + description: 'Indicates whether the current user can invite members to the work item\'s project.' + + def can_invite_members? + Ability.allowed?(current_user, :admin_project_member, object.work_item.project) + end end # rubocop:enable Graphql/AuthorizeTypes end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 2fef4ae98a9..370dbb10462 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -58,14 +58,6 @@ module TreeHelper "#{username}-#{ref}-patch-#{epoch}" end - def tree_edit_project(project = @project) - if can?(current_user, :push_code, project) - project - elsif current_user && current_user.already_forked?(project) - current_user.fork_of(project) - end - end - def edit_in_new_fork_notice_now _("You're not allowed to make changes to this project directly. "\ "A fork of this project is being created that you can make changes in, so you can submit a merge request.") @@ -111,16 +103,6 @@ module TreeHelper end end - def up_dir_path - file = File.join(@path, "..") - tree_join(@ref, file) - end - - # returns the relative path of the first subdir that doesn't have only one directory descendant - def flatten_tree(root_path, tree) - tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') - end - def selected_branch @branch_name || tree_edit_branch end diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb index d900a056242..0fa6a234a3d 100644 --- a/app/models/ci/pending_build.rb +++ b/app/models/ci/pending_build.rb @@ -30,10 +30,6 @@ module Ci self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id) end - def maintain_denormalized_data? - ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data) - end - private def args_from_build(build) @@ -43,13 +39,13 @@ module Ci build: build, project: project, protected: build.protected?, - namespace: project.namespace + namespace: project.namespace, + tag_ids: build.tags_ids, + instance_runners_enabled: shared_runners_enabled?(project) } - if maintain_denormalized_data? - args.store(:tag_ids, build.tags_ids) - args.store(:instance_runners_enabled, shared_runners_enabled?(project)) - args.store(:namespace_traversal_ids, project.namespace.traversal_ids) if group_runners_enabled?(project) + if group_runners_enabled?(project) + args.store(:namespace_traversal_ids, project.namespace.traversal_ids) end args diff --git a/app/models/ci/runner_version.rb b/app/models/ci/runner_version.rb index ddbfad08cbb..a68a3316c76 100644 --- a/app/models/ci/runner_version.rb +++ b/app/models/ci/runner_version.rb @@ -2,11 +2,13 @@ module Ci class RunnerVersion < Ci::ApplicationRecord + include EachBatch include EnumWithNil + include BulkInsertSafe # include this last (see https://docs.gitlab.com/ee/development/insert_into_tables_in_batches.html#prepare-applicationrecords-for-bulk-insertion) enum_with_nil status: { not_processed: nil, - invalid_version: -1, + invalid_version: -1, # Named invalid_version to avoid clash with auto-generated `invalid?` ActiveRecord method unknown: 0, not_available: 1, available: 2, @@ -24,6 +26,10 @@ module Ci # Override auto generated negative scope (from available) so the scope has expected behavior scope :not_available, -> { where(status: :not_available) } + # This scope returns all versions that might need recalculating. For instance, once a version is considered + # :recommended, it normally doesn't change status even if the instance is upgraded + scope :potentially_outdated, -> { where(status: [nil, :not_available, :available, :unknown]) } + validates :version, length: { maximum: 2048 } end end diff --git a/app/models/concerns/loose_index_scan.rb b/app/models/concerns/loose_index_scan.rb index 420ac4d0f62..5d37a30171a 100644 --- a/app/models/concerns/loose_index_scan.rb +++ b/app/models/concerns/loose_index_scan.rb @@ -56,7 +56,8 @@ module LooseIndexScan .project(Arel::Nodes::Grouping.new(Arel.sql(inner_query.to_sql)).as(column.to_s)) unscoped do - with + select(column) + .with .recursive(cte.to_arel) .from(cte.alias_to(arel_table)) .where(arel_column.not_eq(nil)) # filtering out the last NULL value diff --git a/app/services/ci/queue/build_queue_service.rb b/app/services/ci/queue/build_queue_service.rb index fefbdb151ec..2deebc1d725 100644 --- a/app/services/ci/queue/build_queue_service.rb +++ b/app/services/ci/queue/build_queue_service.rb @@ -24,25 +24,7 @@ module Ci # rubocop:disable CodeReuse/ActiveRecord def builds_for_group_runner - if strategy.use_denormalized_data_strategy? - strategy.builds_for_group_runner - else - # Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL` - groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces) - - hierarchy_groups = Gitlab::ObjectHierarchy - .new(groups) - .base_and_descendants - - projects = Project.where(namespace_id: hierarchy_groups) - .with_group_runners_enabled - .with_builds_enabled - .without_deleted - - relation = new_builds.where(project: projects) - - order(relation) - end + strategy.builds_for_group_runner end def builds_for_project_runner @@ -80,11 +62,7 @@ module Ci def strategy strong_memoize(:strategy) do - if ::Feature.enabled?(:ci_pending_builds_queue_source, runner) - Queue::PendingBuildsStrategy.new(runner) - else - Queue::BuildsTableStrategy.new(runner) - end + Queue::PendingBuildsStrategy.new(runner) end end diff --git a/app/services/ci/queue/builds_table_strategy.rb b/app/services/ci/queue/builds_table_strategy.rb deleted file mode 100644 index c27c10bd18d..00000000000 --- a/app/services/ci/queue/builds_table_strategy.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Ci - module Queue - class BuildsTableStrategy - attr_reader :runner - - def initialize(runner) - @runner = runner - end - - # rubocop:disable CodeReuse/ActiveRecord - def builds_for_shared_runner - relation = new_builds - # don't run projects which have not enabled shared runners and builds - .joins('INNER JOIN projects ON ci_builds.project_id = projects.id') - .where(projects: { shared_runners_enabled: true, pending_delete: false }) - .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') - .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') - - if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops) - # if disaster recovery is enabled, we fallback to FIFO scheduling - relation.order('ci_builds.id ASC') - else - # Implement fair scheduling - # this returns builds that are ordered by number of running builds - # we prefer projects that don't use shared runners at all - relation - .joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id = project_builds.project_id") - .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC') - end - end - - def builds_for_group_runner - raise NotImplementedError - end - - def builds_matching_tag_ids(relation, ids) - # pick builds that does not have other tags than runner's one - relation.matches_tag_ids(ids) - end - - def builds_with_any_tags(relation) - # pick builds that have at least one tag - relation.with_any_tags - end - - def order(relation) - relation.order('id ASC') - end - - def new_builds - ::Ci::Build.pending.unstarted - end - - def build_ids(relation) - relation.pluck(:id) - end - - def use_denormalized_data_strategy? - false - end - - private - - def running_builds_for_shared_runners - ::Ci::Build.running - .where(runner: ::Ci::Runner.instance_type) - .group(:project_id) - .select(:project_id, 'COUNT(*) AS running_builds') - end - # rubocop:enable CodeReuse/ActiveRecord - end - end -end diff --git a/app/services/ci/queue/pending_builds_strategy.rb b/app/services/ci/queue/pending_builds_strategy.rb index f2eba0681db..c8bdbba5e65 100644 --- a/app/services/ci/queue/pending_builds_strategy.rb +++ b/app/services/ci/queue/pending_builds_strategy.rb @@ -23,19 +23,11 @@ module Ci end def builds_matching_tag_ids(relation, ids) - if use_denormalized_data_strategy? - relation.for_tags(runner.tags_ids) - else - relation.merge(CommitStatus.matches_tag_ids(ids, table: 'ci_pending_builds', column: 'build_id')) - end + relation.for_tags(runner.tags_ids) end def builds_with_any_tags(relation) - if use_denormalized_data_strategy? - relation.where('cardinality(tag_ids) > 0') - else - relation.merge(CommitStatus.with_any_tags(table: 'ci_pending_builds', column: 'build_id')) - end + relation.where('cardinality(tag_ids) > 0') end def order(relation) @@ -50,23 +42,10 @@ module Ci relation.pluck(:build_id) end - def use_denormalized_data_strategy? - ::Feature.enabled?(:ci_queuing_use_denormalized_data_strategy) - end - private def builds_available_for_shared_runners - if use_denormalized_data_strategy? - new_builds.with_instance_runners - else - new_builds - # don't run projects which have not enabled shared runners and builds - .joins('INNER JOIN projects ON ci_pending_builds.project_id = projects.id') - .where(projects: { shared_runners_enabled: true, pending_delete: false }) - .joins('LEFT JOIN project_features ON ci_pending_builds.project_id = project_features.project_id') - .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') - end + new_builds.with_instance_runners end def builds_ordered_for_shared_runners(relation) diff --git a/app/services/ci/runners/reconcile_existing_runner_versions_service.rb b/app/services/ci/runners/reconcile_existing_runner_versions_service.rb new file mode 100644 index 00000000000..84abf5b8a5e --- /dev/null +++ b/app/services/ci/runners/reconcile_existing_runner_versions_service.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Ci + module Runners + class ReconcileExistingRunnerVersionsService + include BaseServiceUtility + + VERSION_BATCH_SIZE = 100 + + def execute + insert_result = insert_runner_versions + total_deleted = cleanup_runner_versions(insert_result[:versions_from_runners]) + total_updated = update_status_on_outdated_runner_versions(insert_result[:versions_from_runners]) + + success({ + total_inserted: insert_result[:new_record_count], + total_updated: total_updated, + total_deleted: total_deleted + }) + end + + private + + def upgrade_check + Gitlab::Ci::RunnerUpgradeCheck.instance + end + + # rubocop: disable CodeReuse/ActiveRecord + def insert_runner_versions + versions_from_runners = Set[] + new_record_count = 0 + Ci::Runner.distinct_each_batch(column: :version, of: VERSION_BATCH_SIZE) do |version_batch| + batch_versions = version_batch.pluck(:version) + versions_from_runners += batch_versions + new_record_count += Ci::RunnerVersion.bulk_insert!( + version_batch, + returns: :ids, + skip_duplicates: true, + validate: false).count + end + + { versions_from_runners: versions_from_runners, new_record_count: new_record_count } + end + + def cleanup_runner_versions(versions_from_runners) + Ci::RunnerVersion.where.not(version: versions_from_runners).delete_all + end + # rubocop: enable CodeReuse/ActiveRecord + + def outdated_runner_versions + Ci::RunnerVersion.potentially_outdated + end + + def update_status_on_outdated_runner_versions(versions_from_runners) + total_updated = 0 + + outdated_runner_versions.each_batch(of: VERSION_BATCH_SIZE) do |version_batch| + updated = version_batch + .select { |runner_version| versions_from_runners.include?(runner_version['version']) } + .filter_map { |runner_version| runner_version_with_updated_status(runner_version) } + + if updated.any? + total_updated += Ci::RunnerVersion.upsert_all(updated, unique_by: :version).count + end + end + + total_updated + end + + def runner_version_with_updated_status(runner_version) + version = runner_version['version'] + new_status = upgrade_check.check_runner_upgrade_status(version) + + if new_status != :error && new_status != runner_version['status'].to_sym + { + version: version, + status: Ci::RunnerVersion.statuses[new_status] + } + end + end + end + end +end diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index a525ea179e0..58927a90b6e 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -14,8 +14,6 @@ module Ci # Add a build to the pending builds queue # def push(build, transition) - return unless maintain_pending_builds_queue? - raise InvalidQueueTransition unless transition.to == 'pending' transition.within_transaction do @@ -33,8 +31,6 @@ module Ci # Remove a build from the pending builds queue # def pop(build, transition) - return unless maintain_pending_builds_queue? - raise InvalidQueueTransition unless transition.from == 'pending' transition.within_transaction { remove!(build) } @@ -57,7 +53,6 @@ module Ci # Add shared runner build tracking entry (used for queuing). # def track(build, transition) - return unless maintain_pending_builds_queue? return unless build.shared_runner_build? raise InvalidQueueTransition unless transition.to == 'running' @@ -78,7 +73,6 @@ module Ci # queuing). # def untrack(build, transition) - return unless maintain_pending_builds_queue? return unless build.shared_runner_build? raise InvalidQueueTransition unless transition.from == 'running' @@ -115,9 +109,5 @@ module Ci runner.pick_build!(build) end end - - def maintain_pending_builds_queue? - ::Ci::PendingBuild.maintain_denormalized_data? - end end end diff --git a/app/services/ci/update_pending_build_service.rb b/app/services/ci/update_pending_build_service.rb index 733b684bcc6..2118dbcc19e 100644 --- a/app/services/ci/update_pending_build_service.rb +++ b/app/services/ci/update_pending_build_service.rb @@ -15,8 +15,6 @@ module Ci end def execute - return unless ::Ci::PendingBuild.maintain_denormalized_data? - @model.pending_builds.each_batch do |relation| relation.update_all(@update_params) end diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 5c45e37bb27..7bcc97914e5 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,25 +1,42 @@ = gitlab_ui_form_for [:admin, @group] do |f| = form_errors(@group, pajamas_alert: true) - = render 'shared/group_form', f: f - = render 'shared/group_form_description', f: f - - = render 'shared/admin/admin_note_form', f: f - - = render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group - = render_if_exists 'admin/namespace_plan', f: f - - .form-group.gl-form-group{ role: 'group' } - = f.label :avatar, _("Group avatar"), class: 'gl-display-block col-form-label' - = render 'shared/choose_avatar_button', f: f - - = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false - - .form-group.gl-form-group{ role: 'group' } - = render 'shared/allow_request_access', form: f - - = render 'groups/group_admin_settings', f: f - - = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f + .gl-border-b.gl-mb-6 + .row + .col-lg-4 + %h4.gl-mt-0 + = _('Naming, visibility') + %p + = _('Update your group name, description, avatar, and visibility.') + = link_to _('Learn more about groups.'), help_page_path('user/group/index') + .col-lg-8 + = render 'shared/group_form', f: f + = render 'shared/group_form_description', f: f + .form-group.gl-form-group{ role: 'group' } + = f.label :avatar, _("Group avatar"), class: 'gl-display-block col-form-label' + = render 'shared/choose_avatar_button', f: f + = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false + + .gl-border-b.gl-pb-3.gl-mb-6 + .row + .col-lg-4 + %h4.gl-mt-0 + = _('Permissions and group features') + %p + = _('Configure advanced permissions, Large File Storage, two-factor authentication, and CI/CD settings.') + .col-lg-8 + = render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group + = render_if_exists 'admin/namespace_plan', f: f + .form-group.gl-form-group{ role: 'group' } + = render 'shared/allow_request_access', form: f + = render 'groups/group_admin_settings', f: f + = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f + .gl-mb-3 + .row + .col-lg-4 + %h4.gl-mt-0 + = _('Admin notes') + .col-lg-8 + = render 'shared/admin/admin_note_form', f: f - if @group.new_record? = render Pajamas::AlertComponent.new(dismissible: false) do |c| diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 0e69921315a..687a1fb32bf 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -24,6 +24,6 @@ .form-group.gl-form-group{ role: 'group' } = f.label :two_factor_grace_period, _('Two-factor authentication grace period'), class: 'gl-display-block col-form-label' - = f.text_field :two_factor_grace_period, class: 'form-control gl-form-input' + = f.text_field :two_factor_grace_period, class: 'form-control gl-form-input gl-form-input-sm' %small.form-text.text-gl-muted = _("Time (in hours) that users are allowed to skip forced configuration of two-factor authentication.") diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml index 2191d10ecd2..0527d38159b 100644 --- a/app/views/groups/_new_group_fields.html.haml +++ b/app/views/groups/_new_group_fields.html.haml @@ -2,7 +2,7 @@ = render 'shared/group_form', f: f, autofocus: true .row - .form-group.col-sm-12.gl-mb-0 + .form-group.gl-form-group.col-sm-12 %label.label-bold = _('Visibility level') %p diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index 0535205925a..527791dfc04 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -30,6 +30,6 @@ - if @group.avatar? %hr = link_to s_('Groups|Remove avatar'), group_avatar_path(@group.to_param), aria: { label: s_('Groups|Remove avatar') }, data: { confirm: s_('Groups|Avatar will be removed. Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :delete, class: 'gl-button btn btn-danger-secondary' - - = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group - = f.submit s_('Groups|Save changes'), class: 'btn gl-button btn-confirm mt-4 js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' } + .form-group.gl-form-group + = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + = f.submit s_('Groups|Save changes'), class: 'btn gl-button btn-confirm js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' } diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 891860698e7..158b41d78f4 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -52,10 +52,11 @@ - unless Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers? || !Gitlab.com? .js-deployment-target-select -= f.label :visibility_level, class: 'label-bold' do - = s_('ProjectsNew|Visibility Level') - = link_to sprite_icon('question-o'), help_page_path('user/public_access'), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' -= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false, data: { qa_selector: 'visibility_radios'} +.form-group.gl-form-group + = f.label :visibility_level, class: 'label-bold' do + = s_('ProjectsNew|Visibility Level') + = link_to sprite_icon('question-o'), help_page_path('user/public_access'), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' + = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false, data: { qa_selector: 'visibility_radios'} - if !hide_init_with_readme = f.label :project_configuration, class: 'label-bold' do diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 8cd0d2f9e32..cee8d2e92aa 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -67,5 +67,5 @@ %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? - = form_errors(@merge_request) + = form_errors(@merge_request, pajamas_alert: true) = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn gl-mt-4", data: { qa_selector: "compare_branches_button" } diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index b7c0077f61a..763ae5a498b 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,6 +1,6 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.visibility-level-setting +.visibility-level-setting - if with_label = f.label :visibility_level, _('Visibility level'), class: 'label-bold gl-mb-0' %p diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index efb39ee74ce..1bac75e0ff5 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -7,7 +7,7 @@ "#{visibility_level_icon(level)} #{visibility_level_label(level)}".html_safe, help_text: '<span class="option-description">%{visibility_level_description}</span><span class="option-disabled-reason"></span>'.html_safe % { visibility_level_description: visibility_level_description(level, form_model)}, radio_options: { checked: (selected_level == level), data: { track_label: "blank_project", track_action: "activate_form_input", track_property: "#{model_method}_#{level}", track_value: "", qa_selector: "#{visibility_level_label(level).downcase}_radio" } }, - label_options: { class: 'form-check-label gl-mb-2' } + label_options: { class: 'js-visibility-level-radio' } .text-muted diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 6e9f0e8894c..902ae013bd7 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -219,6 +219,15 @@ :weight: 1 :idempotent: false :tags: [] +- :name: cronjob:ci_runners_reconcile_existing_runner_versions_cron + :worker_name: Ci::Runners::ReconcileExistingRunnerVersionsCronWorker + :feature_category: :runner_fleet + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:ci_schedule_delete_objects_cron :worker_name: Ci::ScheduleDeleteObjectsCronWorker :feature_category: :continuous_integration diff --git a/app/workers/ci/runners/reconcile_existing_runner_versions_cron_worker.rb b/app/workers/ci/runners/reconcile_existing_runner_versions_cron_worker.rb new file mode 100644 index 00000000000..035b2563e56 --- /dev/null +++ b/app/workers/ci/runners/reconcile_existing_runner_versions_cron_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ci + module Runners + class ReconcileExistingRunnerVersionsCronWorker + include ApplicationWorker + + # This worker does not schedule other workers that require context. + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext + + data_consistency :sticky + feature_category :runner_fleet + urgency :low + + idempotent! + + def perform + result = ::Ci::Runners::ReconcileExistingRunnerVersionsService.new.execute + result.each { |key, value| log_extra_metadata_on_done(key, value) } + end + end + end +end diff --git a/app/workers/concerns/packages/cleanup_artifact_worker.rb b/app/workers/concerns/packages/cleanup_artifact_worker.rb index a01d7e8abba..7e647ddd229 100644 --- a/app/workers/concerns/packages/cleanup_artifact_worker.rb +++ b/app/workers/concerns/packages/cleanup_artifact_worker.rb @@ -9,14 +9,21 @@ module Packages def perform_work return unless artifact - artifact.transaction do - log_metadata(artifact) + begin + artifact.transaction do + log_metadata(artifact) - artifact.destroy! - rescue StandardError + artifact.destroy! + end + rescue StandardError => exception unless artifact&.destroyed? artifact&.update_column(:status, :error) end + + Gitlab::ErrorTracking.log_exception( + exception, + class: self.class.name + ) end after_destroy diff --git a/config/feature_flags/development/ci_pending_builds_maintain_denormalized_data.yml b/config/feature_flags/development/ci_pending_builds_maintain_denormalized_data.yml deleted file mode 100644 index 51c9fd21564..00000000000 --- a/config/feature_flags/development/ci_pending_builds_maintain_denormalized_data.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_pending_builds_maintain_denormalized_data -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 -milestone: '14.6' -type: development -group: group::pipeline execution -default_enabled: true diff --git a/config/feature_flags/development/ci_pending_builds_queue_source.yml b/config/feature_flags/development/ci_pending_builds_queue_source.yml deleted file mode 100644 index 5fc2fcdb77a..00000000000 --- a/config/feature_flags/development/ci_pending_builds_queue_source.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_pending_builds_queue_source -introduced_by_url: -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 -milestone: '14.0' -type: development -group: group::pipeline execution -default_enabled: true diff --git a/config/feature_flags/development/ci_queuing_use_denormalized_data_strategy.yml b/config/feature_flags/development/ci_queuing_use_denormalized_data_strategy.yml deleted file mode 100644 index 43c7c8e7c2a..00000000000 --- a/config/feature_flags/development/ci_queuing_use_denormalized_data_strategy.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_queuing_use_denormalized_data_strategy -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 -milestone: '14.6' -type: development -group: group::pipeline execution -default_enabled: true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 38d58c93774..78ddc0afa3a 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -540,6 +540,10 @@ production: &base ci_platform_metrics_update_cron_worker: cron: "47 9 * * *" + # Periodically update ci_runner_versions table with up-to-date versions and status. + ci_runner_versions_reconciliation_worker: + cron: "20 * * * *" + # GitLab EE only jobs. These jobs are automatically enabled for an EE # installation, and ignored for a CE installation. ee_cron_jobs: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index dca82e73075..2de4efbe8ef 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -630,6 +630,9 @@ Settings.cron_jobs['inactive_projects_deletion_cron_worker']['job_class'] = 'Pro Settings.cron_jobs['loose_foreign_keys_cleanup_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['loose_foreign_keys_cleanup_worker']['job_class'] = 'LooseForeignKeys::CleanupWorker' +Settings.cron_jobs['ci_runner_versions_reconciliation_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['cron'] ||= '20 * * * *' +Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['job_class'] = 'Ci::Runners::ReconcileExistingRunnerVersionsCronWorker' Gitlab.ee do Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) diff --git a/danger/ci_config/Dangerfile b/danger/ci_config/Dangerfile index 5022ccc1539..f29888d1ec8 100644 --- a/danger/ci_config/Dangerfile +++ b/danger/ci_config/Dangerfile @@ -9,6 +9,7 @@ def get_ci_config_files(files) end schema_path = 'app/assets/javascripts/editor/schema/ci.json' +schema_docs_path = 'https://docs.gitlab.com/ee/development/cicd/schema.html#write-specs' has_schema_update = all_changed_files.include?(schema_path) return if has_schema_update @@ -17,4 +18,4 @@ return if ci_config_files.empty? file_list = "- #{ci_config_files.map { |path| "`#{path}`" }.join("\n- ")}" -warn "This merge request changed CI config files but did not update the schema. Please consider updating [the schema](#{schema_path}) to reflect these changes:\n#{file_list}" +warn "This merge request changed CI config files but did not update the schema. Please consider updating [the schema](#{schema_path}) to reflect these changes:\n#{file_list}.\n\nRefer to the [docs](#{schema_docs_path}) for help on how to run and write specs for the CI schema." diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ed9842870cd..4612ff76960 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -16419,6 +16419,7 @@ Represents a release. | <a id="releasedescription"></a>`description` | [`String`](#string) | Description (also known as "release notes") of the release. | | <a id="releasedescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | | <a id="releaseevidences"></a>`evidences` | [`ReleaseEvidenceConnection`](#releaseevidenceconnection) | Evidence for the release. (see [Connections](#connections)) | +| <a id="releasehistoricalrelease"></a>`historicalRelease` | [`Boolean`](#boolean) | Indicates the release is an historical release. | | <a id="releaseid"></a>`id` | [`ReleaseID!`](#releaseid) | Global ID of the release. | | <a id="releaselinks"></a>`links` | [`ReleaseLinks`](#releaselinks) | Links of the release. | | <a id="releasemilestones"></a>`milestones` | [`MilestoneConnection`](#milestoneconnection) | Milestones associated to the release. (see [Connections](#connections)) | @@ -18498,6 +18499,7 @@ Represents an assignees widget. | ---- | ---- | ----------- | | <a id="workitemwidgetassigneesallowsmultipleassignees"></a>`allowsMultipleAssignees` | [`Boolean`](#boolean) | Indicates whether multiple assignees are allowed. | | <a id="workitemwidgetassigneesassignees"></a>`assignees` | [`UserCoreConnection`](#usercoreconnection) | Assignees of the work item. (see [Connections](#connections)) | +| <a id="workitemwidgetassigneescaninvitemembers"></a>`canInviteMembers` | [`Boolean!`](#boolean) | Indicates whether the current user can invite members to the work item's project. | | <a id="workitemwidgetassigneestype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. | ### `WorkItemWidgetDescription` diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md index 2e91d45181e..e80bffe7c18 100644 --- a/doc/development/adding_database_indexes.md +++ b/doc/development/adding_database_indexes.md @@ -141,16 +141,7 @@ created with one or more of the following options: ### Considerations for index names -Index names don't have any significance in the database, so they should -attempt to communicate intent to others. The most important rule to -remember is that generic names are more likely to conflict or be duplicated, -and should not be used. Some other points to consider: - -- For general indexes, use a template, like: `index_{table}_{column}_{options}`. -- For indexes added to solve a very specific problem, it may make sense - for the name to reflect their use. -- Identifiers in PostgreSQL have a maximum length of 63 bytes. -- Check `db/structure.sql` for conflicts and ideas. +Check our [Constraints naming conventions](database/constraint_naming_convention.md) page. ### Why explicit names are required @@ -205,8 +196,7 @@ that would otherwise not be used. In these cases, consider a temporary index. To specify a temporary index: -1. Prefix the index name with `tmp_` and follow the [naming conventions](database/constraint_naming_convention.md) - and [requirements for naming indexes](#requirements-for-naming-indexes) for the rest of the name. +1. Prefix the index name with `tmp_` and follow the [naming conventions](database/constraint_naming_convention.md). 1. Create a follow-up issue to remove the index in the next (or future) milestone. 1. Add a comment in the migration mentioning the removal issue. diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 9b8785dee24..1225260e600 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -71,34 +71,34 @@ It picks reviewers and maintainers from the list at the [engineering projects](https://about.gitlab.com/handbook/engineering/projects/) page, with these behaviors: -1. It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status): - - Contains the string `OOO`, `PTO`, `Parental Leave`, or `Friends and Family`. - - GitLab user **Busy** indicator is set to `True`. - - Emoji is from one of these categories: - - **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:` - - **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:` - - **At capacity** - 🔴 `:red_circle:` - - **Focus mode** - 💡 `:bulb:` (focusing on their team's work) -1. It doesn't pick people who are already assigned a number of reviews that is equal to - or greater than their chosen "review limit". The review limit is the maximum number of - reviews people are ready to handle at a time. Set a review limit by using one of the following - as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status): - - 0️⃣ - `:zero:` (similar to `:red_circle:`) - - 1️⃣ - `:one:` - - 2️⃣ - `:two:` - - 3️⃣ - `:three:` - - 4️⃣ - `:four:` - - 5️⃣ - `:five:` -1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji - is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers. - - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers. - - [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers. -1. People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji - is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked. -1. It always picks the same reviewers and maintainers for the same - branch name (unless their out-of-office (`OOO`) status changes, as in point 1). It - removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so - that it can be stable for backport branches. +- It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status): + - Contains the string `OOO`, `PTO`, `Parental Leave`, or `Friends and Family`. + - GitLab user **Busy** indicator is set to `True`. + - Emoji is from one of these categories: + - **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:` + - **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:` + - **At capacity** - 🔴 `:red_circle:` + - **Focus mode** - 💡 `:bulb:` (focusing on their team's work) +- It doesn't pick people who are already assigned a number of reviews that is equal to + or greater than their chosen "review limit". The review limit is the maximum number of + reviews people are ready to handle at a time. Set a review limit by using one of the following + as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status): + - 0️⃣ - `:zero:` (similar to `:red_circle:`) + - 1️⃣ - `:one:` + - 2️⃣ - `:two:` + - 3️⃣ - `:three:` + - 4️⃣ - `:four:` + - 5️⃣ - `:five:` +- Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji + is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers. + - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers. + - [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers. +- People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji + is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked. +- It always picks the same reviewers and maintainers for the same + branch name (unless their out-of-office (`OOO`) status changes, as in point 1). It + removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so + that it can be stable for backport branches. The [Roulette dashboard](https://gitlab-org.gitlab.io/gitlab-roulette) contains: diff --git a/doc/development/database/constraint_naming_convention.md b/doc/development/database/constraint_naming_convention.md index 72f16c20559..2f0b8bf0463 100644 --- a/doc/development/database/constraint_naming_convention.md +++ b/doc/development/database/constraint_naming_convention.md @@ -25,5 +25,7 @@ The intent is not to retroactively change names in existing databases but rather ## Observations +- Check `db/structure.sql` for conflicts. - Prefixes are preferred over suffices because they make it easier to identify the type of a given constraint quickly, as well as group them alphabetically; - The `_and_` that joins column names can be omitted to keep the identifiers under the 63 characters' length limit defined by PostgreSQL. Additionally, the notation may be abbreviated to the best of our ability if struggling to keep under this limit. +- For indexes added to solve a very specific problem, it may make sense for the name to reflect their use. diff --git a/doc/development/packages/debian_repository.md b/doc/development/packages/debian_repository.md new file mode 100644 index 00000000000..6f65e04878b --- /dev/null +++ b/doc/development/packages/debian_repository.md @@ -0,0 +1,151 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Debian Repository + +This guide explains: + +1. A basic overview of how Debian packages are structured +1. What package managers, clients, and tools are used to manage Debian packages +1. How the GitLab Debian repository functions + +## Debian package basics + +There are two types of [Debian packages](https://www.debian.org/doc/manuals/debian-faq/pkg-basics.en.html): binary and source. + +- **Binary** - These are usually `.deb` files and contain executables, config files, and other data. A binary package must match your OS or architecture since it is already compiled. These are usually installed using `dpkg`. Dependencies must already exist on the system when installing a binary package. +- **Source** - These are usual made up of `.dsc` files and `.gz` files. A source package is compiled on your system. These are fetched and installed with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html), which then uses `dpkg` after the package is compiled. When you use `apt`, it will fetch and install the necessary dependencies. + +The `.deb` file follows the naming convention `<PackageName>_<VersionNumber>-<DebianRevisionNumber>_<DebianArchitecture>.deb` + +It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info <deb_file>` + +The [`.changes` file](https://www.debian.org/doc/debian-policy/ch-controlfields.html#debian-changes-files-changes) is used to tell the Debian repository how to process updates to packages. It contains a variety of metadata for the package, including architecture, distribution, and version. In addition to the metadata, they contain three lists of checksums: `sha1`, `sha256`, and `md5` in the `Files` section. Refer to [sample_1.2.3~alpha2_amd64.changes](https://gitlab.com/gitlab-org/gitlab/-/blob/dd1e70d3676891025534dc4a1e89ca9383178fe7/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes) for an example of how these files are structured. + +## How do people get Debian packages? + +While you can download a single `.deb` file and install it with [`dpkg`](https://manpages.debian.org/bullseye/dpkg/dpkg.1.en.html), most users consume Debian packages with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html) using `apt-get`. `apt` wraps `dpkg`, adding dependency management and compilation. + +## How do people publish Debian packages? + +It is not uncommon to use `curl` to publish packages depending on the type of Debian repository you are working with. However, `dput-ng` is the best tool to use as it will upload the relevant files based on the `.changes` file. + +## What is all this distribution business? + +When it comes to Debian, packages don't exist on their own. They belong to a _distribution_. This can mean many things, but the main thing to note is users are used to having to specify the distribution. + +## What does a Debian Repository look like? + +- A [Debian repository](https://wiki.debian.org/DebianRepository) is made up of many releases. +- Each release is given a **codename**. For the public Debian repository, these are things like "bullseye" and "jesse". + - There is also the concept of **suites** which are essentially aliases of codenames synonymous with release channels like "stable" and "edge". +- Each release has many **components**. In the public repository, these are "main", "contrib", and "non-free". +- Each release has many **architectures** such as "amd64", "arm64", or "i386". +- Each release has a signed **Release** file (see below about [GPG signing](#what-are-gpg-keys-and-what-are-signed-releases)) + +A standard directory-based Debian repository would be organized as: + +```plaintext +dists\ + |--jessie/ + |--bullseye\ + |Changelog + |Release + |InRelease + |Release.gpg + |--main\ + |--amd64\ + |--arm64\ + |--contrib\ + |--non-free\ +pool\ + |--this is where the .deb files for all releases live +``` + +You can explore a mirror of the public Debian repository here: <http://ftp.us.debian.org/debian/> + +In the public Debian repository, the entire directory structure, release files, GPG keys, and other files are all generated by a series of scripts called the [Debian Archive Kit, or dak](https://salsa.debian.org/ftp-team/dak). + +In the GitLab Debian repository, we don't deal with specific file directories. Instead, we use code and an underlying [PostgreSQL database to organize the relationships](structure.md#debian-packages) between these different pieces. + +## What does a Debian Repository do? + +The Debian community created many package repository systems before things like object storage existed, and they used FTP to upload artifacts to a remote server. Most current package repositories and registries are just directories on a server somewhere. Packages added to the [official Debian distribution](https://www.debian.org/distrib/packages) exist in a central public repository that a group of open source maintainers curates. The package maintainers use the [Debian Archive Kit, or dak](https://salsa.debian.org/ftp-team/dak) scripts to generate release files and do other maintenance tasks. So, in addition to storing and serving files, a complete Debian repository needs to accomplish the same behavior that dak provides. This behavior is what the GitLab Debian registry aims to do. + +## What are GPG keys, and what are signed releases + +A [GPG key](https://www.gnupg.org/) is a public/private key pair for secure data transmission. Similar to an SSH key, there is a private and public key. Whoever has the _public key can encrypt data_, and whoever has the _private key can decrypt data_ that was encrypted using the public key. You can also use GPG keys to sign data. Whoever has the private key can sign data or a file, and whoever has the public key can then check the signature and trust it came from the person with the matching private key. + +We use GPG to sign the release file for the Debian packages. The release file is an index of all packages within a given distribution and their respective digests. + +In the GitLab Debian registry, a background process generates a new release file whenever a user publishes a new package to their Debian repository. A GPG key is created for each distribution. If a user requests a release for that distribution, they can request the signed version and the public GPG key to verify the authenticity of that release file. + +## GitLab repository internals + +When a [file upload](../../api/packages/debian.md#upload-a-package-file) occurs: + +1. A new "incoming" package record is found or created. All new files are assigned to the "incoming" package. It is a holding area used until we know what package the file is actually associated with. +1. A new "unknown" file is stored. It is unknown because we do not yet know if this file belongs to an existing package or not. + +Once we know which package the file belongs to, it is associated with that package, and the "incoming" package is removed if no more files remain. The "unknown" status of the file is updated to the correct file type. + +Next, if the file is a `.changes` format: + +1. The `.changes` file is parsed and any files listed within it are updated. All uploaded non-`.changes` files are correctly associated with various distributions and packages. +1. The `::Packages::Debian::GenerateDistributionWorker` and thus `::Packages::Debian::GenerateDistributionService` are run. + 1. Component files are created or updated. Since we just updated package files that were listed in the `.changes` file, we now check the component/architecture files based on the changed checksum values. + 1. A new release is generated: + 1. A new GPG key is generated if one does not already exist for the distribution + 1. A [Release file](https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files) is written, signed by the GPG key, and then stored. + 1. Old component files are destroyed. + +This diagram shows the path taken after a file is uploaded to the Debian API: + +```mermaid +sequenceDiagram + Client->>+DebianProjectPackages: PUT projects/:id/packages/debian/:file_name + DebianProjectPackages->>+FindOrCreateIncomingService: Create "incoming" package + DebianProjectPackages->>+CreatePackageFileService: Create "unknown" file + Note over DebianProjectPackages: If `.changes` file + DebianProjectPackages->>+ProcessChangesWorker: + DebianProjectPackages->>+Client: 202 Created + ProcessChangesWorker->>+ProcessChangesService: + ProcessChangesService->>+ExtractChangesMetadataService: + ExtractChangesMetadataService->>+ExtractMetadataService: + ExtractMetadataService->>+ParseDebian822Service: + ExtractMetadataService->>+ExtractDebMetadataService: If .deb or .udeb + ExtractDebMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file + ParseDebian822Service-->>-ExtractDebMetadataService: Parse String as Debian RFC822 control data format + ExtractDebMetadataService-->>-ExtractMetadataService: Return the parsed control file + ExtractMetadataService->>+ParseDebian822Service: if .dsc, .changes, or buildinfo + ParseDebian822Service-->>-ExtractMetadataService: Parse String as Debian RFC822 control data format + ExtractMetadataService-->>-ExtractChangesMetadataService: Parse Metadata file + ExtractChangesMetadataService-->>-ProcessChangesService: Return list of files and hashes from the .changes file + loop process files listed in .changes + ProcessChangesService->>+ExtractMetadataService: + ExtractMetadataService->>+ParseDebian822Service: + ExtractMetadataService->>+ExtractDebMetadataService: If .deb or .udeb + ExtractDebMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file + ParseDebian822Service-->>-ExtractDebMetadataService: Parse String as Debian RFC822 control data format + ExtractDebMetadataService-->>-ExtractMetadataService: Return the parsed control file + ExtractMetadataService->>+ParseDebian822Service: if .dsc, .changes, or buildinfo + ParseDebian822Service-->>-ExtractMetadataService: Parse String as Debian RFC822 control data format + ExtractMetadataService-->>-ProcessChangesService: Use parsed metadata to update "unknown" (or known) file + end + ProcessChangesService->>+GenerateDistributionWorker: + GenerateDistributionWorker->>+GenerateDistributionService: + GenerateDistributionService->>+GenerateDistributionService: generate component files based on new archs and updates from .changes + GenerateDistributionService->>+GenerateDistributionKeyService: generate GPG key for distribution + GenerateDistributionKeyService-->>-GenerateDistributionService: GPG key + GenerateDistributionService-->>-GenerateDistributionService: Generate distribution file + GenerateDistributionService->>+SignDistributionService: Sign release file with GPG key + SignDistributionService-->>-GenerateDistributionService: Save the signed release file + GenerateDistributionWorker->>+GenerateDistributionService: destroy no longer used component files +``` + +### Distributions + +You must create a distribution before publishing a package to it. When you create or update a distribution using the project or group distribution API, in addition to creating the initial backing records in the database, the `GenerateDistributionService` run as shown in the above sequence diagram. diff --git a/doc/development/packages/structure.md b/doc/development/packages/structure.md index a2716232b11..f8d9da2cc73 100644 --- a/doc/development/packages/structure.md +++ b/doc/development/packages/structure.md @@ -39,7 +39,6 @@ erDiagram projects }|--|| namespaces : "" packages_packages }|--|| projects : "" packages_package_files }o--|| packages_packages : "" - package_debian_file_metadatum |o--|| packages_package_files : "" packages_debian_group_architectures }|--|| packages_debian_group_distributions : "" packages_debian_group_component_files }|--|| packages_debian_group_components : "" packages_debian_group_component_files }|--|| packages_debian_group_architectures : "" diff --git a/doc/operations/index.md b/doc/operations/index.md index 88687c2faf1..05ce1c5d876 100644 --- a/doc/operations/index.md +++ b/doc/operations/index.md @@ -77,29 +77,14 @@ microservices-based distributed systems - and displays results within GitLab. - [Trace the performance and health](tracing.md) of a deployed application. -## Aggregate and store logs (DEPRECATED) **(FREE SELF)** +<!--- start_remove The following content will be removed on remove_date: '2022-10-18'---> -> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346485) in GitLab 14.7. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/360182) behind a [feature flag](../administration/feature_flags.md) named `monitor_logging` in GitLab 15.0. Disabled by default. +## Aggregate and store logs (removed) **(FREE SELF)** -WARNING: -This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346485) -in GitLab 14.7. -It will be removed completely in GitLab 15.2. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `monitor_logging`. -On GitLab.com, this feature is not available. -This feature is not recommended for production use. - -Developers need to troubleshoot application changes in development, and incident -responders need aggregated, real-time logs when troubleshooting problems with -production services. GitLab provides centralized, aggregated log storage for your -distributed application, enabling you to collect logs across multiple services and -infrastructure. - -- [View logs of pods](../user/project/clusters/kubernetes_pod_logs.md) - in connected Kubernetes clusters. +This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346485) in GitLab 14.7 +and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/360193) in GitLab 15.2. + +<!--- end_remove --> ## Manage your infrastructure in code diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md index 152e3dca27d..bd1f75b7b42 100644 --- a/doc/operations/metrics/dashboards/index.md +++ b/doc/operations/metrics/dashboards/index.md @@ -135,9 +135,6 @@ The options are: - **Expand panel** - Displays a larger version of a visualization. To return to the dashboard, select the **Back** button in your browser, or press the <kbd>Escape</kbd> key. ([Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3100) in GitLab 13.0.) -- **View logs** **(ULTIMATE)** - Displays [Logs](../../../user/project/clusters/kubernetes_pod_logs.md), - if they are enabled. If used in conjunction with the [timeline zoom](#timeline-zoom-and-url-sharing) - feature, logs narrow down to the selected time range. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122013) in GitLab 12.8.) - **Download CSV** - Data from Prometheus charts on the metrics dashboard can be downloaded as CSV. - [Copy link to chart](../embed.md#embedding-gitlab-managed-kubernetes-metrics) diff --git a/doc/topics/application_development_platform/index.md b/doc/topics/application_development_platform/index.md index 1560ceeed26..fac9f963a98 100644 --- a/doc/topics/application_development_platform/index.md +++ b/doc/topics/application_development_platform/index.md @@ -65,4 +65,3 @@ responsibility. The Application Development Platform integrates key performance into GitLab, automatically. The following features are included: - [Auto Monitoring](../autodevops/stages.md#auto-monitoring) -- [In-app Kubernetes Logs](../../user/project/clusters/kubernetes_pod_logs.md) diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 4ec3c8d0297..0d2b68e154d 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -43,22 +43,17 @@ This workflow is considered push-based, because GitLab is pushing requests from GitLab supports the following Kubernetes versions. You can upgrade your Kubernetes version to a supported version at any time: -- 1.24 (support ends on September 22, 2023) -- 1.23 (support ends on February 22, 2023) +- 1.24 (support ends on September 22, 2023 or when 1.27 becomes supported) +- 1.23 (support ends on February 22, 2023 or when 1.26 becomes supported) - 1.22 (support ends on October 22, 2022) -- 1.21 (support ends on September 22, 2022) +- 1.21 (support ends on August 22, 2022) -GitLab supports at least two production-ready Kubernetes minor -versions at any given time. GitLab regularly reviews the supported versions and -provides a three-month deprecation period before removing support for a specific -version. The list of supported versions is based on: +GitLab aims to support a new minor Kubernetes version three months after its initial release. GitLab supports at least three production-ready Kubernetes minor +versions at any given time. -- The versions supported by major managed Kubernetes providers. -- The versions [supported by the Kubernetes community](https://kubernetes.io/releases/version-skew-policy/#supported-versions). +Support for deprecated APIs can be removed from the GitLab codebase when we drop support for the Kubernetes version that only supports the deprecated API. -[This epic](https://gitlab.com/groups/gitlab-org/-/epics/4827) tracks support for other Kubernetes versions. - -Some GitLab features might work on versions not listed here. +Some GitLab features might work on versions not listed here. [This epic](https://gitlab.com/groups/gitlab-org/-/epics/4827) tracks support for Kubernetes versions. ## Migrate to the agent from the legacy certificate-based integration diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index e0334eda875..71d7b7fbb0c 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -34,6 +34,7 @@ To create an epic in the group you're in: - To [make the epic confidential](#make-an-epic-confidential), select the checkbox under **Confidentiality**. - Choose labels. - Select a start and due date, or [inherit](#start-and-due-date-inheritance) them. + - Select a [color](#epic-color). 1. Select **Create epic**. The newly created epic opens. @@ -62,6 +63,18 @@ Because the epic's dates can inherit dates from its children, the start date and If the start date of a child epic on the lowest level changes, that becomes the earliest possible start date for its parent epic. The parent epic's start date then reflects this change and propagates upwards to the top epic. +### Epic color + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79940) in GitLab 14.9 [with a flag](../../../administration/feature_flags.md) named `epic_color_highlight`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `epic_color_highlight`. +On GitLab.com, this feature is available but can be configured by GitLab.com administrators only. +The feature is not ready for production use. + +When you create or edit an epic, you can select its color. +An epic's color is shown in [roadmaps](../roadmap/index.md), and [epic boards](epic_boards.md). + ## Edit an epic After you create an epic, you can edit the following details: @@ -71,6 +84,7 @@ After you create an epic, you can edit the following details: - Start date - Due date - Labels +- [Color](#epic-color) Prerequisites: diff --git a/doc/user/infrastructure/clusters/index.md b/doc/user/infrastructure/clusters/index.md index 4fa5dde31b4..9c8bcd9289c 100644 --- a/doc/user/infrastructure/clusters/index.md +++ b/doc/user/infrastructure/clusters/index.md @@ -59,7 +59,6 @@ This feature flag re-enables the certificate-based Kubernetes integration. - [Cluster environments](../../clusters/environments.md) - [Show Canary Ingress deployments on deploy boards](../../project/canary_deployments.md#show-canary-ingress-deployments-on-deploy-boards-deprecated) - [Deploy Boards](../../project/deploy_boards.md) -- [Pod logs](../../project/clusters/kubernetes_pod_logs.md) - [Clusters health](manage/clusters_health.md) - [Web terminals](../../../administration/integration/terminal.md) diff --git a/doc/user/packages/helm_repository/index.md b/doc/user/packages/helm_repository/index.md index 88ea5afad3c..07e853fa18c 100644 --- a/doc/user/packages/helm_repository/index.md +++ b/doc/user/packages/helm_repository/index.md @@ -69,6 +69,11 @@ Once built, a chart can be uploaded to the desired channel with `curl` or `helm - `<project_id>`: the project ID (like `42`). - `<channel>`: the name of the channel (like `stable`). +### Release channels + +You can publish Helm charts to channels in GitLab. Channels are a method you can use to differentiate Helm chart repositories. +For example, you can use `stable` and `devel` as channels to allow users to add the `stable` repo while `devel` charts are isolated. + ## Use CI/CD to publish a Helm package To publish a Helm package automated through [GitLab CI/CD](../../../ci/index.md), you can use diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 801c107e371..186dcb90f17 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -69,7 +69,6 @@ The following table lists project permissions available for each role: | [Application security](application_security/index.md):<br>View [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ | | [Application security](application_security/index.md):<br>Create a [CVE ID Request](application_security/cve_id_request.md) | | | | ✓ | ✓ | | [Application security](application_security/index.md):<br>Create or assign [security policy project](application_security/policies/index.md) | | | | | ✓ | -| [Clusters](infrastructure/clusters/index.md):<br>View [pod logs](project/clusters/kubernetes_pod_logs.md) | | | ✓ | ✓ | ✓ | | [Clusters](infrastructure/clusters/index.md):<br>View clusters | | | ✓ | ✓ | ✓ | | [Clusters](infrastructure/clusters/index.md):<br>Manage clusters | | | | ✓ | ✓ | | [Container Registry](packages/container_registry/index.md):<br>Create, edit, delete [cleanup policies](packages/container_registry/index.md#delete-images-by-using-a-cleanup-policy) | | | | ✓ | ✓ | diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md index e28371a42ed..bd87ab1024d 100644 --- a/doc/user/project/clusters/kubernetes_pod_logs.md +++ b/doc/user/project/clusters/kubernetes_pod_logs.md @@ -2,85 +2,11 @@ stage: Monitor group: Respond info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +remove_date: '2022-18-10' +redirect_to: '../../clusters/agent/index.md' --- -# Kubernetes Logs (DEPRECATED) **(FREE SELF)** +# Kubernetes Logs (removed) **(FREE SELF)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4752) in GitLab 11.0. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26383) from GitLab Ultimate to GitLab Free 12.9. -> - [Deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/360182) behind a [feature flag](../../../administration/feature_flags.md) named `monitor_logging` in GitLab 15.0. Disabled by default. -> - [Disabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/353410) in GitLab 15.0. - -WARNING: -This feature is in its end-of-life process. -This feature was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5. -It will be [removed completely](https://gitlab.com/gitlab-org/gitlab/-/issues/346485) in GitLab 15.2. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `monitor_logging` and the one named `certificate_based_clusters`. -On GitLab.com, this feature is not available. -This feature is not recommended for production use. - -GitLab makes it easy to view the logs of running pods in -[connected Kubernetes clusters](index.md). By displaying the logs directly in GitLab -in the **Log Explorer**, developers can avoid managing console tools or jumping -to a different interface. The **Log Explorer** interface provides a set of filters -above the log file data, depending on your configuration: - -![Pod logs](img/kubernetes_pod_logs_v12_10.png) - -- **Namespace** - Select the environment to display. Users with Maintainer or - greater [permissions](../../permissions.md) can also see pods in the - `gitlab-managed-apps` namespace. -- **Scroll to bottom** **{scroll_down}** - Scroll to the end of the displayed logs. -- **Refresh** **{retry}** - Reload the displayed logs. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -To learn more about the Log Explorer, see [APM - Log Explorer](https://www.youtube.com/watch?v=hWclZHA7Dgw). - -[Learn more about Kubernetes + GitLab](https://about.gitlab.com/solutions/kubernetes/). -Everything you need to build, test, deploy, and run your application at scale. - -## Requirements - -[Deploying to a Kubernetes environment](../deploy_boards.md#enabling-deploy-boards) -is required to use Logs. - -## Accessing the log explorer - -To access the **Log explorer**, select the **More actions** **{ellipsis_v}** menu on -a [metrics dashboard](../../../operations/metrics/index.md) and select **View logs**, or: - -1. Sign in as a user with the _View pod logs_ - [permissions](../../permissions.md#project-members-permissions) in the project. -1. To navigate to the **Log Explorer** from the sidebar menu, go to **Monitor > Logs** - ([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 12.5.). -1. To navigate to the **Log Explorer** from a specific pod on a [deploy board](../deploy_boards.md): - - 1. Go to **Deployments > Environments** and find the environment - which contains the desired pod, like `production`. - 1. On the **Environments** page, you should see the status of the environment's - pods with [deploy boards](../deploy_boards.md). - 1. When mousing over the list of pods, GitLab displays a tooltip with the exact pod name - and status. - ![deploy boards pod list](img/pod_logs_deploy_board.png) - 1. Select the desired pod to display the **Log Explorer**. - -### Logs view - -The **Log Explorer** lets you filter the logs by: - -- Pods. -- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/issues/5769), environments. -- [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/197879), dates. -- [From GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/208790), managed apps. - -Loading more than 500 log lines is possible from -[GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/198050) onward. - -Support for pods with multiple containers is coming -[in a future release](https://gitlab.com/gitlab-org/gitlab/-/issues/13404). - -Support for historical data is coming -[in a future release](https://gitlab.com/gitlab-org/gitlab/-/issues/196191). +This feature was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5 +and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/360193) in GitLab 15.2. diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index 02a9b76ce38..c4ae00f3c6c 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -24,6 +24,13 @@ members. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/352526) in GitLab 14.9. [Feature flag `invite_members_group_modal`](https://gitlab.com/gitlab-org/gitlab/-/issues/352526) removed. +You can share a project only with: + +- Groups for which you have an explicitly defined [membership](index.md). +- Groups that contain a nested subgroup or project for which you have an explicitly defined role. + +Administrators can share projects with any group in the namespace. + The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? @@ -42,12 +49,11 @@ After sharing 'Project Acme' with 'Engineering': - The group is listed in the **Groups** tab. - The project is listed on the group dashboard. -You can share a project only with: - -- Groups for which you have an explicitly defined membership. -- Groups that contain a nested subgroup or project for which you have an explicitly defined role. +When you share a project, be aware of the following restrictions and outcomes: -Administrators can share projects with any group in the system. +- [Maximum access level](#maximum-access-level) +- [Sharing a public project with a private group](#share-a-public-project-with-private-group) +- [Sharing project with group lock](#share-project-with-group-lock) ## Maximum access level @@ -61,9 +67,13 @@ in. That means you can only share down the hierarchy. For example, `group/subgro - Can not be shared with `group`. - Can be shared with `group/subgroup02` or `group/subgroup01/subgroup03`. -## Share public project with private group +## Share a public project with private group + +When you share a public project with a private group, be aware of the following outcomes: -When sharing a public project with a private group, owners and maintainers of the project see the name of the group in the `members` page. Owners also have the possibility to see members of the private group they don't have access to when mentioning them in the issue or merge request. +- The name of the group is no longer private and is visible to all users in the project members page. +- Owners of the project have access to members of the private group when they mention them in issues or merge requests. +- Project members who are direct or indirect members of the private group can see private group members listed in addition to members of the project. ## Share project with group lock diff --git a/lib/gitlab/ci/queue/metrics.rb b/lib/gitlab/ci/queue/metrics.rb index 7d8303214a5..5cee73238ca 100644 --- a/lib/gitlab/ci/queue/metrics.rb +++ b/lib/gitlab/ci/queue/metrics.rb @@ -250,11 +250,7 @@ module Gitlab end def running_jobs_relation(job) - if ::Feature.enabled?(:ci_pending_builds_maintain_denormalized_data) - ::Ci::RunningBuild.instance_type.where(project_id: job.project_id) - else - job.project.builds.running.where(runner: ::Ci::Runner.instance_type) - end + ::Ci::RunningBuild.instance_type.where(project_id: job.project_id) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb index d09e69500a3..98ecf3a0dd8 100644 --- a/lib/gitlab/ci/runner_upgrade_check.rb +++ b/lib/gitlab/ci/runner_upgrade_check.rb @@ -8,7 +8,7 @@ module Gitlab def check_runner_upgrade_status(runner_version) runner_version = ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true) - return :invalid unless runner_version.valid? + return :invalid_version unless runner_version.valid? releases = RunnerReleases.instance.releases return :error unless releases diff --git a/lib/gitlab/git/rugged_impl/tree.rb b/lib/gitlab/git/rugged_impl/tree.rb index 40c003821b9..bc0af12d7e3 100644 --- a/lib/gitlab/git/rugged_impl/tree.rb +++ b/lib/gitlab/git/rugged_impl/tree.rb @@ -63,10 +63,7 @@ module Gitlab def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive) tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries| # This was an optimization to reduce N+1 queries for Gitaly - # (https://gitlab.com/gitlab-org/gitaly/issues/530). It - # used to be done lazily in the view via - # TreeHelper#flatten_tree, so it's possible there's a - # performance impact by loading this eagerly. + # (https://gitlab.com/gitlab-org/gitaly/issues/530). rugged_populate_flat_path(repository, sha, path, entries) end end diff --git a/lib/gitlab/github_import/importer/events/renamed.rb b/lib/gitlab/github_import/importer/events/renamed.rb new file mode 100644 index 00000000000..6a11c492210 --- /dev/null +++ b/lib/gitlab/github_import/importer/events/renamed.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + module Events + class Renamed + def initialize(project, user_id) + @project = project + @user_id = user_id + end + + # issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent` + def execute(issue_event) + Note.create!(note_params(issue_event)) + end + + private + + attr_reader :project, :user_id + + def note_params(issue_event) + { + noteable_id: issue_event.issue_db_id, + noteable_type: Issue.name, + project_id: project.id, + author_id: user_id, + note: parse_body(issue_event), + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at, + system_note_metadata: SystemNoteMetadata.new( + { + action: "title", + created_at: issue_event.created_at, + updated_at: issue_event.created_at + } + ) + } + end + + def parse_body(issue_event) + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new( + issue_event.old_title, issue_event.new_title + ).inline_diffs + + marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(issue_event.old_title).mark(old_diffs) + marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(issue_event.new_title).mark(new_diffs) + + "changed title from **#{marked_old_title}** to **#{marked_new_title}**" + end + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/issue_event_importer.rb b/lib/gitlab/github_import/importer/issue_event_importer.rb index 86e6e8ff309..955cfa5a0d0 100644 --- a/lib/gitlab/github_import/importer/issue_event_importer.rb +++ b/lib/gitlab/github_import/importer/issue_event_importer.rb @@ -27,6 +27,9 @@ module Gitlab when 'labeled', 'unlabeled' Gitlab::GithubImport::Importer::Events::ChangedLabel.new(project, author_id) .execute(issue_event) + when 'renamed' + Gitlab::GithubImport::Importer::Events::Renamed.new(project, author_id) + .execute(issue_event) else Gitlab::GithubImport::Logger.debug( message: 'UNSUPPORTED_EVENT_TYPE', diff --git a/lib/gitlab/github_import/representation/issue_event.rb b/lib/gitlab/github_import/representation/issue_event.rb index a04beeec5f2..f62507a331b 100644 --- a/lib/gitlab/github_import/representation/issue_event.rb +++ b/lib/gitlab/github_import/representation/issue_event.rb @@ -9,7 +9,7 @@ module Gitlab attr_reader :attributes - expose_attribute :id, :actor, :event, :commit_id, :label_title, :created_at + expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title, :created_at expose_attribute :issue_db_id # set in SingleEndpointIssueEventsImporter#each_associated # Builds a event from a GitHub API response. @@ -22,6 +22,8 @@ module Gitlab event: event.event, commit_id: event.commit_id, label_title: event.label && event.label[:name], + old_title: event.rename && event.rename[:from], + new_title: event.rename && event.rename[:to], issue_db_id: event.issue_db_id, created_at: event.created_at ) @@ -30,7 +32,7 @@ module Gitlab # Builds a event using a Hash that was built from a JSON payload. def self.from_json_hash(raw_hash) hash = Representation.symbolize_hash(raw_hash) - hash[:actor] = Representation::User.from_json_hash(hash[:actor]) if hash[:actor] + hash[:actor] &&= Representation::User.from_json_hash(hash[:actor]) new(hash) end diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb index 85f0ba1fd25..72df8b423df 100644 --- a/lib/gitlab/tree_summary.rb +++ b/lib/gitlab/tree_summary.rb @@ -8,10 +8,7 @@ module Gitlab CACHE_EXPIRE_IN = 1.hour MAX_OFFSET = 2**31 - attr_reader :commit, :project, :path, :offset, :limit, :user - - attr_reader :resolved_commits - private :resolved_commits + attr_reader :commit, :project, :path, :offset, :limit, :user, :resolved_commits def initialize(commit, project, user, params = {}) @commit = commit @@ -34,44 +31,37 @@ module Gitlab # # - An Array of Hashes containing the following keys: # - file_name: The full path of the tree entry - # - type: One of :blob, :tree, or :submodule # - commit: The last ::Commit to touch this entry in the tree # - commit_path: URI of the commit in the web interface - # - An Array of the unique ::Commit objects in the first value + # - commit_title_html: Rendered commit title def summarize - summary = contents - .tap { |summary| fill_last_commits!(summary) } - - [summary, commits] - end - - def fetch_logs - logs, _ = summarize + commits_hsh = fetch_last_cached_commits_list + prerender_commit_full_titles!(commits_hsh.values) - new_offset = next_offset if more? + commits_hsh.map do |path_key, commit| + commit = cache_commit(commit) - [logs.as_json, new_offset] + { + file_name: File.basename(path_key).force_encoding(Encoding::UTF_8), + commit: commit, + commit_path: commit_path(commit), + commit_title_html: markdown_field(commit, :full_title) + } + end end - # Does the tree contain more entries after the given offset + limit? - def more? - all_contents[next_offset].present? - end + def fetch_logs + logs = summarize - # The offset of the next batch of tree entries. If more? returns false, this - # batch will be empty - def next_offset - [all_contents.size + 1, offset + limit].min + [logs.first(limit).as_json, next_offset(logs.size)] end private - def contents - all_contents[offset, limit] || [] - end + def next_offset(entries_count) + return if entries_count <= limit - def commits - resolved_commits.values + offset + limit end def repository @@ -83,32 +73,12 @@ module Gitlab File.join(*[path, ""]) if path end - def entry_path(entry) - File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT) - end - - def fill_last_commits!(entries) - commits_hsh = fetch_last_cached_commits_list - prerender_commit_full_titles!(commits_hsh.values) - - entries.each do |entry| - path_key = entry_path(entry) - commit = cache_commit(commits_hsh[path_key]) - - if commit - entry[:commit] = commit - entry[:commit_path] = commit_path(commit) - entry[:commit_title_html] = markdown_field(commit, :full_title) - end - end - end - def fetch_last_cached_commits_list - cache_key = ['projects', project.id, 'last_commits', commit.id, ensured_path, offset, limit] + cache_key = ['projects', project.id, 'last_commits', commit.id, ensured_path, offset, limit + 1] commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do repository - .list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true) + .list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit + 1, literal_pathspec: true) .transform_values! { |commit| commit_to_hash(commit) } end @@ -131,26 +101,6 @@ module Gitlab Gitlab::Routing.url_helpers.project_commit_path(project, commit) end - def all_contents - strong_memoize(:all_contents) { cached_contents } - end - - def cached_contents - cache_key = ['projects', project.id, 'content', commit.id, path] - - Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do - [ - *tree.trees, - *tree.blobs, - *tree.submodules - ].map { |entry| { file_name: entry.name, type: entry.type } } - end - end - - def tree - strong_memoize(:tree) { repository.tree(commit.id, path) } - end - def prerender_commit_full_titles!(commits) # Preload commit authors as they are used in rendering commits.each(&:lazy_author) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7104f962000..35dd74bbd52 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9669,6 +9669,9 @@ msgstr "" msgid "Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" msgstr "" +msgid "Configure advanced permissions, Large File Storage, two-factor authentication, and CI/CD settings." +msgstr "" + msgid "Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings." msgstr "" diff --git a/package.json b/package.json index 2fa89bdb9b9..b8cc7505b45 100644 --- a/package.json +++ b/package.json @@ -200,9 +200,9 @@ "yaml": "^2.0.0-10" }, "devDependencies": { - "@gitlab/eslint-plugin": "13.0.0", + "@gitlab/eslint-plugin": "13.1.0", "@gitlab/stylelint-config": "4.1.0", - "@graphql-eslint/eslint-plugin": "3.10.4", + "@graphql-eslint/eslint-plugin": "3.10.5", "@testing-library/dom": "^7.16.2", "@types/jest": "^26.0.24", "@vue/test-utils": "1.3.0", diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 36b230103db..c5a97812d1f 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -178,7 +178,8 @@ RSpec.describe RegistrationsController do category: 'RegistrationsController', action: 'accepted', label: 'invite_email', - property: member.id.to_s + property: member.id.to_s, + user: member.reload.user ) end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 9311cb63abb..fe804dc52d7 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -221,7 +221,8 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do category: 'RegistrationsController', action: 'accepted', label: 'invite_email', - property: group_invite.id.to_s + property: group_invite.id.to_s, + user: group_invite.reload.user ) end end diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb index 3f89e344c51..15d889933bf 100644 --- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb +++ b/spec/features/projects/pipelines/legacy_pipelines_spec.rb @@ -357,6 +357,10 @@ RSpec.describe 'Pipelines', :js do end it 'enqueues the delayed job', :js do + find('[data-testid="mini-pipeline-graph-dropdown"]').click + + within('[data-testid="mini-pipeline-graph-dropdown"]') { find('.ci-status-icon-pending') } + expect(delayed_job.reload).to be_pending end end diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb index bed6c798793..154084e0181 100644 --- a/spec/frontend/fixtures/deploy_keys.rb +++ b/spec/frontend/fixtures/deploy_keys.rb @@ -27,9 +27,9 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c render_views it 'deploy_keys/keys.json' do - create(:rsa_deploy_key_2048, public: true) - project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') - internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') + create(:rsa_deploy_key_5120, public: true) + project_key = create(:deploy_key) + internal_key = create(:deploy_key) create(:deploy_keys_project, project: project, deploy_key: project_key) create(:deploy_keys_project, project: project2, deploy_key: internal_key) create(:deploy_keys_project, project: project3, deploy_key: project_key) diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js index 5186c9a8992..e3b4dcb8acc 100644 --- a/spec/frontend/repository/log_tree_spec.js +++ b/spec/frontend/repository/log_tree_spec.js @@ -16,19 +16,18 @@ const mockData = [ commit_path: `https://test.com`, commit_title_html: 'commit title', file_name: 'index.js', - type: 'blob', }, ]; describe('resolveCommit', () => { it('calls resolve when commit found', () => { const resolver = { - entry: { name: 'index.js', type: 'blob' }, + entry: { name: 'index.js' }, resolve: jest.fn(), }; const commits = [ - { fileName: 'index.js', filePath: '/index.js', type: 'blob' }, - { fileName: 'index.js', filePath: '/app/assets/index.js', type: 'blob' }, + { fileName: 'index.js', filePath: '/index.js' }, + { fileName: 'index.js', filePath: '/app/assets/index.js' }, ]; resolveCommit(commits, '', resolver); @@ -36,7 +35,6 @@ describe('resolveCommit', () => { expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', filePath: '/index.js', - type: 'blob', }); }); }); @@ -56,7 +54,7 @@ describe('fetchLogsTree', () => { global.gon = { relative_url_root: '' }; resolver = { - entry: { name: 'index.js', type: 'blob' }, + entry: { name: 'index.js' }, resolve: jest.fn(), }; @@ -119,7 +117,6 @@ describe('fetchLogsTree', () => { filePath: '/index.js', message: 'testing message', sha: '123', - type: 'blob', }), ); })); @@ -136,7 +133,6 @@ describe('fetchLogsTree', () => { message: 'testing message', sha: '123', titleHtml: 'commit title', - type: 'blob', }), ], }); diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js index aaaa39f739f..b3dd5118308 100644 --- a/spec/frontend/repository/utils/commit_spec.js +++ b/spec/frontend/repository/utils/commit_spec.js @@ -10,7 +10,6 @@ const mockData = [ commit_path: `https://test.com`, commit_title_html: 'testing message', file_name: 'index.js', - type: 'blob', }, ]; @@ -24,7 +23,6 @@ describe('normalizeData', () => { commitPath: 'https://test.com', fileName: 'index.js', filePath: '/index.js', - type: 'blob', titleHtml: 'testing message', __typename: 'LogTreeCommit', }, diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index 28e7d192938..433be5d5027 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -9,6 +9,7 @@ import { redirectTo } from '~/lib/utils/url_utility'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; +import RunnerDetails from '~/runner/components/runner_details.vue'; import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; @@ -37,6 +38,7 @@ describe('AdminRunnerShowApp', () => { let mockRunnerQuery; const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton); const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton); const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton); @@ -179,12 +181,32 @@ describe('AdminRunnerShowApp', () => { }); }); + describe('When loading', () => { + beforeEach(() => { + mockRunnerQueryResult(); + + createComponent(); + }); + + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + + it('does not show runner jobs', () => { + expect(findRunnersJobs().exists()).toBe(false); + }); + }); + describe('When there is an error', () => { beforeEach(async () => { mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); await createComponent(); }); + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + it('error is reported to sentry', () => { expect(captureException).toHaveBeenCalledWith({ error: new Error('Error!'), @@ -201,13 +223,6 @@ describe('AdminRunnerShowApp', () => { const stubs = { GlTab, GlTabs, - RunnerDetails: { - template: ` - <div> - <slot name="jobs-tab"></slot> - </div> - `, - }, }; it('without a runner, shows no jobs', () => { diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index 9e0f7014750..552ee29b6f9 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -25,12 +25,7 @@ describe('RunnerDetails', () => { const findDetailGroups = () => wrapper.findComponent(RunnerGroups); - const createComponent = ({ - props = {}, - stubs, - mountFn = shallowMountExtended, - ...options - } = {}) => { + const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerDetails, { propsData: { ...props, @@ -39,7 +34,6 @@ describe('RunnerDetails', () => { RunnerDetail, ...stubs, }, - ...options, }); }; @@ -47,16 +41,6 @@ describe('RunnerDetails', () => { wrapper.destroy(); }); - it('when no runner is present, no contents are shown', () => { - createComponent({ - props: { - runner: null, - }, - }); - - expect(wrapper.text()).toBe(''); - }); - describe('Details tab', () => { describe.each` field | runner | expectedValue @@ -141,18 +125,4 @@ describe('RunnerDetails', () => { }); }); }); - - describe('Jobs tab slot', () => { - it('shows job tab slot', () => { - const JOBS_TAB = '<div>Jobs Tab</div>'; - - createComponent({ - slots: { - 'jobs-tab': JOBS_TAB, - }, - }); - - expect(wrapper.html()).toContain(JOBS_TAB); - }); - }); }); diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js index c13176ace15..2065874c288 100644 --- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js +++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js @@ -177,12 +177,28 @@ describe('GroupRunnerShowApp', () => { }); }); + describe('When loading', () => { + beforeEach(() => { + mockRunnerQueryResult(); + + createComponent(); + }); + + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + }); + describe('When there is an error', () => { beforeEach(async () => { mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); await createComponent(); }); + it('does not show runner details', () => { + expect(findRunnerDetails().exists()).toBe(false); + }); + it('error is reported to sentry', () => { expect(captureException).toHaveBeenCalledWith({ error: new Error('Error!'), diff --git a/spec/graphql/types/release_type_spec.rb b/spec/graphql/types/release_type_spec.rb index 0c05a68c5a6..a1dc8850f94 100644 --- a/spec/graphql/types/release_type_spec.rb +++ b/spec/graphql/types/release_type_spec.rb @@ -11,7 +11,8 @@ RSpec.describe GitlabSchema.types['Release'] do description description_html name milestones evidences author commit assets links - created_at released_at + created_at released_at upcoming_release + historical_release ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/work_items/widgets/assignees_type_spec.rb b/spec/graphql/types/work_items/widgets/assignees_type_spec.rb index 2db0667faea..816e66f1db1 100644 --- a/spec/graphql/types/work_items/widgets/assignees_type_spec.rb +++ b/spec/graphql/types/work_items/widgets/assignees_type_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Types::WorkItems::Widgets::AssigneesType do it 'exposes the expected fields' do - expected_fields = %i[assignees allows_multiple_assignees type] + expected_fields = %i[assignees allows_multiple_assignees can_invite_members type] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 026432adf99..c40284ee933 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -3,63 +3,12 @@ require 'spec_helper' RSpec.describe TreeHelper do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' } let_it_be(:user) { create(:user) } - def create_file(filename) - project.repository.create_file( - project.creator, - filename, - 'test this', - message: "Automatically created file #{filename}", - branch_name: 'master' - ) - end - - describe 'flatten_tree' do - let(:tree) { repository.tree(sha, 'files') } - let(:root_path) { 'files' } - let(:tree_item) { tree.entries.find { |entry| entry.path == path } } - - subject { flatten_tree(root_path, tree_item) } - - context "on a directory containing more than one file/directory" do - let(:path) { 'files/html' } - - it "returns the directory name" do - expect(subject).to match('html') - end - end - - context "on a directory containing only one directory" do - let(:path) { 'files/flat' } - - it "returns the flattened path" do - expect(subject).to match('flat/path/correct') - end - - context "with a nested root path" do - let(:root_path) { 'files/flat' } - - it "returns the flattened path with the root path suffix removed" do - expect(subject).to match('path/correct') - end - end - end - - context 'when the root path contains a plus character' do - let(:root_path) { 'gtk/C++' } - let(:tree_item) { double(flat_path: 'gtk/C++/glade') } - - it 'returns the flattened path' do - expect(subject).to eq('glade') - end - end - end - describe '#commit_in_single_accessible_branch' do it 'escapes HTML from the branch name' do helper.instance_variable_set(:@branch_name, "<script>alert('escape me!');</script>") @@ -163,6 +112,7 @@ RSpec.describe TreeHelper do context 'user does not have write access but a personal fork exists' do include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:forked_project) { create(:project, :repository, namespace: user.namespace) } before do diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index 6640c394dfa..392f64a5fea 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -65,16 +65,16 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with nil runner_version' do let(:runner_version) { nil } - it 'returns :invalid' do - is_expected.to eq(:invalid) + it 'returns :invalid_version' do + is_expected.to eq(:invalid_version) end end context 'with invalid runner_version' do let(:runner_version) { 'junk' } - it 'returns :invalid' do - is_expected.to eq(:invalid) + it 'returns :invalid_version' do + is_expected.to eq(:invalid_version) end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 3a34d39c722..d5d1bef7bff 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::GitalyClient::CommitService do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let(:storage_name) { project.repository_storage } let(:relative_path) { project.disk_path + '.git' } let(:repository) { project.repository } diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb new file mode 100644 index 00000000000..a8c3fbcb05d --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do + subject(:importer) { described_class.new(project, user.id) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => 'renamed', + 'commit_id' => nil, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'old_title' => 'old title', + 'new_title' => 'new title', + 'issue_db_id' => issue.id + ) + end + + let(:expected_note_attrs) do + { + noteable_id: issue.id, + noteable_type: Issue.name, + project_id: project.id, + author_id: user.id, + note: "changed title from **{-old-} title** to **{+new+} title**", + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_system_note_metadata_attrs) do + { + action: "title", + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + describe '#execute' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issue.notes.count } + .from(0).to(1) + + expect(issue.notes.last) + .to have_attributes(expected_note_attrs) + end + + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } + .from(0).to(1) + + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) + ) + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb index 645f4113eb6..5e8fe26d3de 100644 --- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -80,6 +80,13 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab Gitlab::GithubImport::Importer::Events::ChangedLabel end + context "when it's renamed issue event" do + let(:event_name) { 'renamed' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::Renamed + end + context "when it's unknown issue event" do let(:event_name) { 'fake' } diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb index 2cd8969ef6b..d36037a257d 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -57,6 +57,22 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do end end + context 'when rename field is present' do + it 'includes the old_title and new_title fields' do + expect(issue_event.old_title).to eq('old title') + expect(issue_event.new_title).to eq('new title') + end + end + + context 'when rename field is empty' do + let(:with_rename) { false } + + it 'does not return such info' do + expect(issue_event.old_title).to eq nil + expect(issue_event.new_title).to eq nil + end + end + it 'includes the created timestamp' do expect(issue_event.created_at).to eq('2022-04-26 18:30:53 UTC') end @@ -73,7 +89,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:response) do event_resource = Struct.new( :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, - :issue_db_id, :created_at, :performed_via_github_app, + :rename, :issue_db_id, :created_at, :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -86,6 +102,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d', commit_url: 'https://api.github.com/repos/octocat/Hello-World/commits'\ '/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', + rename: with_rename ? { from: 'old title', to: 'new title' } : nil, issue_db_id: 100500, label: with_label ? { name: 'label title' } : nil, created_at: '2022-04-26 18:30:53 UTC', @@ -95,6 +112,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_actor) { true } let(:with_label) { true } + let(:with_rename) { true } it_behaves_like 'an IssueEvent' do let(:issue_event) { described_class.from_api_response(response) } @@ -114,6 +132,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 'commit_url' => 'https://api.github.com/repos/octocat/Hello-World/commits/570e7b2abdd848b95f2f578043fc23bd6f6fd24d', 'label_title' => (with_label ? 'label title' : nil), + 'old_title' => with_rename ? 'old title' : nil, + 'new_title' => with_rename ? 'new title' : nil, "issue_db_id" => 100500, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil @@ -122,6 +142,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_actor) { true } let(:with_label) { true } + let(:with_rename) { true } let(:issue_event) { described_class.from_json_hash(hash) } end diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index 3021d92244e..f45005fcc9b 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -30,50 +30,31 @@ RSpec.describe Gitlab::TreeSummary do describe '#summarize' do let(:project) { create(:project, :custom_repo, files: { 'a.txt' => '' }) } - subject(:summarized) { summary.summarize } + subject(:entries) { summary.summarize } - it 'returns an array of entries, and an array of commits' do - expect(summarized).to be_a(Array) - expect(summarized.size).to eq(2) + it 'returns an array of entries' do + expect(entries).to be_a(Array) + expect(entries.size).to eq(1) - entries, commits = *summarized aggregate_failures do expect(entries).to contain_exactly( a_hash_including(file_name: 'a.txt', commit: have_attributes(id: commit.id)) ) - expect(commits).to match_array(entries.map { |entry| entry[:commit] }) - end - end - - context 'when offset is over the limit' do - let(:offset) { 100 } - - it 'returns an empty array' do - expect(summarized).to eq([[], []]) + expect(summary.resolved_commits.values).to match_array(entries.map { |entry| entry[:commit] }) end end context 'with caching', :use_clean_rails_memory_store_caching do subject { Rails.cache.fetch(key) } - context 'Repository tree cache' do - let(:key) { ['projects', project.id, 'content', commit.id, path] } - - it 'creates a cache for repository content' do - summarized - - is_expected.to eq([{ file_name: 'a.txt', type: :blob }]) - end - end - context 'Commits list cache' do let(:offset) { 0 } let(:limit) { 25 } - let(:key) { ['projects', project.id, 'last_commits', commit.id, path, offset, limit] } + let(:key) { ['projects', project.id, 'last_commits', commit.id, path, offset, limit + 1] } it 'creates a cache for commits list' do - summarized + entries is_expected.to eq('a.txt' => commit.to_hash) end @@ -93,7 +74,7 @@ RSpec.describe Gitlab::TreeSummary do let(:expected_message) { message[0...1021] + '...' } it 'truncates commit message to 1 kilobyte' do - summarized + entries is_expected.to include('long.txt' => a_hash_including(message: expected_message)) end @@ -102,7 +83,7 @@ RSpec.describe Gitlab::TreeSummary do end end - describe '#summarize (entries)' do + describe '#fetch_logs' do let(:limit) { 4 } custom_files = { @@ -116,33 +97,32 @@ RSpec.describe Gitlab::TreeSummary do let!(:project) { create(:project, :custom_repo, files: custom_files) } let(:commit) { repo.head_commit } - subject(:entries) { summary.summarize.first } + subject(:entries) { summary.fetch_logs.first } it 'summarizes the entries within the window' do is_expected.to contain_exactly( - a_hash_including(type: :tree, file_name: 'directory'), - a_hash_including(type: :blob, file_name: 'a.txt'), - a_hash_including(type: :blob, file_name: ':file'), - a_hash_including(type: :tree, file_name: ':dir') + a_hash_including('file_name' => 'directory'), + a_hash_including('file_name' => 'a.txt'), + a_hash_including('file_name' => ':file'), + a_hash_including('file_name' => ':dir') # b.txt is excluded by the limit ) end it 'references the commit and commit path in entries' do # There are 2 trees and the summary is not ordered - entry = entries.find { |entry| entry[:commit].id == commit.id } + entry = entries.find { |entry| entry['commit']['id'] == commit.id } expected_commit_path = Gitlab::Routing.url_helpers.project_commit_path(project, commit) - expect(entry[:commit]).to be_a(::Commit) - expect(entry[:commit_path]).to eq(expected_commit_path) - expect(entry[:commit_title_html]).to eq(commit.message) + expect(entry['commit_path']).to eq(expected_commit_path) + expect(entry['commit_title_html']).to eq(commit.message) end context 'in a good subdirectory' do let(:path) { 'directory' } it 'summarizes the entries in the subdirectory' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'c.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'c.txt')) end end @@ -150,7 +130,7 @@ RSpec.describe Gitlab::TreeSummary do let(:path) { ':dir' } it 'summarizes the entries in the subdirectory' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'test.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'test.txt')) end end @@ -164,7 +144,25 @@ RSpec.describe Gitlab::TreeSummary do let(:offset) { 4 } it 'returns entries from the offset' do - is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'b.txt')) + is_expected.to contain_exactly(a_hash_including('file_name' => 'b.txt')) + end + end + + context 'next offset' do + subject { summary.fetch_logs.last } + + context 'when there are more entries to fetch' do + it 'returns next offset' do + is_expected.to eq(4) + end + end + + context 'when there are no more entries to fetch' do + let(:limit) { 5 } + + it 'returns next offset' do + is_expected.to be_nil + end end end end @@ -178,10 +176,11 @@ RSpec.describe Gitlab::TreeSummary do let(:project) { create(:project, :repository) } let(:commit) { repo.commit(test_commit_sha) } let(:limit) { nil } - let(:entries) { summary.summarize.first } + let(:entries) { summary.summarize } subject(:commits) do - summary.summarize.last + summary.summarize + summary.resolved_commits.values end it 'returns an Array of ::Commit objects' do @@ -227,7 +226,7 @@ RSpec.describe Gitlab::TreeSummary do let_it_be(:project) { create(:project, :empty_repo) } let_it_be(:issue) { create(:issue, project: project) } - let(:entries) { summary.summarize.first } + let(:entries) { summary.summarize } let(:entry) { entries.find { |entry| entry[:file_name] == 'issue.txt' } } before_all do @@ -264,67 +263,6 @@ RSpec.describe Gitlab::TreeSummary do end end - describe '#more?' do - let(:path) { 'tmp/more' } - - where(:num_entries, :offset, :limit, :expected_result) do - 0 | 0 | 0 | false - 0 | 0 | 1 | false - - 1 | 0 | 0 | true - 1 | 0 | 1 | false - 1 | 1 | 0 | false - 1 | 1 | 1 | false - - 2 | 0 | 0 | true - 2 | 0 | 1 | true - 2 | 0 | 2 | false - 2 | 0 | 3 | false - 2 | 1 | 0 | true - 2 | 1 | 1 | false - 2 | 2 | 0 | false - 2 | 2 | 1 | false - end - - with_them do - before do - create_file('dummy', path: 'other') if num_entries == 0 - 1.upto(num_entries) { |n| create_file(n, path: path) } - end - - subject { summary.more? } - - it { is_expected.to eq(expected_result) } - end - end - - describe '#next_offset' do - let(:path) { 'tmp/next_offset' } - - where(:num_entries, :offset, :limit, :expected_result) do - 0 | 0 | 0 | 0 - 0 | 0 | 1 | 1 - 0 | 1 | 0 | 1 - 0 | 1 | 1 | 1 - - 1 | 0 | 0 | 0 - 1 | 0 | 1 | 1 - 1 | 1 | 0 | 1 - 1 | 1 | 1 | 2 - end - - with_them do - before do - create_file('dummy', path: 'other') if num_entries == 0 - 1.upto(num_entries) { |n| create_file(n, path: path) } - end - - subject { summary.next_offset } - - it { is_expected.to eq(expected_result) } - end - end - def create_file(unique, path:) repo.create_file( project.creator, diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb index 5692444339f..4bb43233dbd 100644 --- a/spec/models/ci/pending_build_spec.rb +++ b/spec/models/ci/pending_build_spec.rb @@ -118,41 +118,27 @@ RSpec.describe Ci::PendingBuild do project.shared_runners_enabled = true end - context 'when ci_pending_builds_maintain_denormalized_data is enabled' do - it 'sets instance_runners_enabled to true' do - described_class.upsert_from_build!(build) - - expect(described_class.last.instance_runners_enabled).to be_truthy - end - - context 'when project is about to be deleted' do - before do - build.project.update!(pending_delete: true) - end + it 'sets instance_runners_enabled to true' do + described_class.upsert_from_build!(build) - it 'sets instance_runners_enabled to false' do - described_class.upsert_from_build!(build) + expect(described_class.last.instance_runners_enabled).to be_truthy + end - expect(described_class.last.instance_runners_enabled).to be_falsey - end + context 'when project is about to be deleted' do + before do + build.project.update!(pending_delete: true) end - context 'when builds are disabled' do - before do - build.project.project_feature.update!(builds_access_level: false) - end - - it 'sets instance_runners_enabled to false' do - described_class.upsert_from_build!(build) + it 'sets instance_runners_enabled to false' do + described_class.upsert_from_build!(build) - expect(described_class.last.instance_runners_enabled).to be_falsey - end + expect(described_class.last.instance_runners_enabled).to be_falsey end end - context 'when ci_pending_builds_maintain_denormalized_data is disabled' do + context 'when builds are disabled' do before do - stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false) + build.project.project_feature.update!(builds_access_level: false) end it 'sets instance_runners_enabled to false' do @@ -168,24 +154,10 @@ RSpec.describe Ci::PendingBuild do subject(:ci_pending_build) { described_class.last } - context 'when ci_pending_builds_maintain_denormalized_data is enabled' do - it 'sets tag_ids' do - described_class.upsert_from_build!(build) - - expect(ci_pending_build.tag_ids).to eq(build.tags_ids) - end - end - - context 'when ci_pending_builds_maintain_denormalized_data is disabled' do - before do - stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false) - end - - it 'does not set tag_ids' do - described_class.upsert_from_build!(build) + it 'sets tag_ids' do + described_class.upsert_from_build!(build) - expect(ci_pending_build.tag_ids).to be_empty - end + expect(ci_pending_build.tag_ids).to eq(build.tags_ids) end end diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb index 0303e0bb657..d3395942a39 100644 --- a/spec/models/ci/runner_version_spec.rb +++ b/spec/models/ci/runner_version_spec.rb @@ -5,13 +5,40 @@ require 'spec_helper' RSpec.describe Ci::RunnerVersion do it_behaves_like 'having unique enum values' + let_it_be(:runner_version_not_available) do + create(:ci_runner_version, version: 'abc123', status: :not_available) + end + + let_it_be(:runner_version_recommended) do + create(:ci_runner_version, version: 'abc234', status: :recommended) + end + describe '.not_available' do subject { described_class.not_available } - let!(:runner_version1) { create(:ci_runner_version, version: 'abc123', status: :not_available) } - let!(:runner_version2) { create(:ci_runner_version, version: 'abc234', status: :recommended) } + it { is_expected.to match_array([runner_version_not_available]) } + end + + describe '.potentially_outdated' do + subject { described_class.potentially_outdated } - it { is_expected.to match_array([runner_version1]) } + let_it_be(:runner_version_nil) { create(:ci_runner_version, version: 'abc345', status: nil) } + let_it_be(:runner_version_available) do + create(:ci_runner_version, version: 'abc456', status: :available) + end + + let_it_be(:runner_version_unknown) do + create(:ci_runner_version, version: 'abc567', status: :unknown) + end + + it 'contains any runner version that is not already recommended' do + is_expected.to match_array([ + runner_version_nil, + runner_version_not_available, + runner_version_available, + runner_version_unknown + ]) + end end describe 'validation' do diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb index b7a8276ec55..20d786f311f 100644 --- a/spec/models/tree_spec.rb +++ b/spec/models/tree_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Tree do - let(:repository) { create(:project, :repository).repository } + let_it_be(:repository) { create(:project, :repository).repository } + let(:sha) { repository.root_ref } subject(:tree) { described_class.new(repository, '54fcc214') } diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 70fa8100411..f17d2ebbb7e 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -200,6 +200,7 @@ RSpec.describe 'Query.work_item(id)' do type ... on WorkItemWidgetAssignees { allowsMultipleAssignees + canInviteMembers assignees { nodes { id @@ -218,6 +219,7 @@ RSpec.describe 'Query.work_item(id)' do hash_including( 'type' => 'ASSIGNEES', 'allowsMultipleAssignees' => boolean, + 'canInviteMembers' => boolean, 'assignees' => { 'nodes' => match_array( assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 74adbc4efc8..2316575f164 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -750,41 +750,7 @@ module Ci end context 'when using pending builds table' do - before do - stub_feature_flags(ci_pending_builds_queue_source: true) - end - - context 'with ci_queuing_use_denormalized_data_strategy enabled' do - before do - stub_feature_flags(ci_queuing_use_denormalized_data_strategy: true) - end - - include_examples 'handles runner assignment' - end - - context 'with ci_queuing_use_denormalized_data_strategy disabled' do - before do - skip_if_multiple_databases_are_setup - - stub_feature_flags(ci_queuing_use_denormalized_data_strategy: false) - end - - around do |example| - allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332952') do - example.run - end - end - - include_examples 'handles runner assignment' - end - - context 'with ci_queuing_use_denormalized_data_strategy enabled' do - before do - stub_feature_flags(ci_queuing_use_denormalized_data_strategy: true) - end - - include_examples 'handles runner assignment' - end + include_examples 'handles runner assignment' context 'when a conflicting data is stored in denormalized table' do let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) } @@ -805,22 +771,6 @@ module Ci end end end - - context 'when not using pending builds table' do - before do - skip_if_multiple_databases_are_setup - - stub_feature_flags(ci_pending_builds_queue_source: false) - end - - around do |example| - allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332952') do - example.run - end - end - - include_examples 'handles runner assignment' - end end describe '#register_success' do @@ -888,14 +838,6 @@ module Ci shared_examples 'metrics collector' do it_behaves_like 'attempt counter collector' it_behaves_like 'jobs queueing time histogram collector' - - context 'when using denormalized data is disabled' do - before do - stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false) - end - - it_behaves_like 'jobs queueing time histogram collector' - end end context 'when shared runner is used' do diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb new file mode 100644 index 00000000000..8a225494d5a --- /dev/null +++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' do + subject(:execute) { described_class.new.execute } + + let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') } + let_it_be(:runner_version_14_0_1) do + create(:ci_runner_version, version: '14.0.1', status: :not_available) + end + + before do + stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1) + + allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) + .to receive(:check_runner_upgrade_status) + .and_return(:recommended) + end + + context 'with runner with new version' do + let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') } + let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) } + let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') } + + before do + allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) + .to receive(:check_runner_upgrade_status) + .with('14.0.2') + .and_return(:not_available) + .once + end + + it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do + result = nil + expect { result = execute } + .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended') + .and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended') + .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available') + + expect(result).to eq({ + status: :success, + total_inserted: 1, # 14.0.2 is inserted + total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated + total_deleted: 0 + }) + end + end + + context 'with orphan ci_runner_version' do + let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) } + + before do + allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) + .to receive(:check_runner_upgrade_status) + .and_return(:not_available) + end + + it 'deletes orphan ci_runner_versions entry', :aggregate_failures do + result = nil + expect { result = execute } + .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil) + .and not_change { runner_version_14_0_1.reload.status }.from('not_available') + + expect(result).to eq({ + status: :success, + total_inserted: 0, + total_updated: 0, + total_deleted: 1 # 14.0.2 is deleted + }) + end + end + + context 'with no runner version changes' do + before do + allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) + .to receive(:check_runner_upgrade_status) + .and_return(:not_available) + end + + it 'does not modify ci_runner_versions entries', :aggregate_failures do + result = nil + expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + + expect(result).to eq({ + status: :success, + total_inserted: 0, + total_updated: 0, + total_deleted: 0 + }) + end + end + + context 'with failing version check' do + before do + allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) + .to receive(:check_runner_upgrade_status) + .and_return(:error) + end + + it 'makes no changes to ci_runner_versions', :aggregate_failures do + result = nil + expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + + expect(result).to eq({ + status: :success, + total_inserted: 0, + total_updated: 0, + total_deleted: 0 + }) + end + end +end diff --git a/spec/services/ci/update_pending_build_service_spec.rb b/spec/services/ci/update_pending_build_service_spec.rb index 2bb0aded24a..e49b22299f0 100644 --- a/spec/services/ci/update_pending_build_service_spec.rb +++ b/spec/services/ci/update_pending_build_service_spec.rb @@ -42,19 +42,6 @@ RSpec.describe Ci::UpdatePendingBuildService do expect(pending_build_1.instance_runners_enabled).to be_truthy expect(pending_build_2.instance_runners_enabled).to be_truthy end - - context 'when ci_pending_builds_maintain_denormalized_data is disabled' do - before do - stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false) - end - - it 'does not update all pending builds', :aggregate_failures do - update_pending_builds - - expect(pending_build_1.instance_runners_enabled).to be_falsey - expect(pending_build_2.instance_runners_enabled).to be_truthy - end - end end context 'when model is a project with pending builds' do @@ -66,19 +53,6 @@ RSpec.describe Ci::UpdatePendingBuildService do expect(pending_build_1.instance_runners_enabled).to be_truthy expect(pending_build_2.instance_runners_enabled).to be_truthy end - - context 'when ci_pending_builds_maintain_denormalized_data is disabled' do - before do - stub_feature_flags(ci_pending_builds_maintain_denormalized_data: false) - end - - it 'does not update all pending builds', :aggregate_failures do - update_pending_builds - - expect(pending_build_1.instance_runners_enabled).to be_falsey - expect(pending_build_2.instance_runners_enabled).to be_truthy - end - end end end end diff --git a/spec/workers/packages/cleanup_package_file_worker_spec.rb b/spec/workers/packages/cleanup_package_file_worker_spec.rb index 380e8916d13..95cf65c18c5 100644 --- a/spec/workers/packages/cleanup_package_file_worker_spec.rb +++ b/spec/workers/packages/cleanup_package_file_worker_spec.rb @@ -52,6 +52,7 @@ RSpec.describe Packages::CleanupPackageFileWorker do end it 'handles the error' do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(RuntimeError), class: described_class.name) expect { subject }.to change { Packages::PackageFile.error.count }.from(0).to(1) expect(package_file.reload).to be_error end @@ -71,7 +72,9 @@ RSpec.describe Packages::CleanupPackageFileWorker do end it 'handles the error' do - expect { subject }.to change { Packages::PackageFile.count }.by(-1) + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(RuntimeError), class: described_class.name) + expect { subject }.not_to change { Packages::PackageFile.count } + expect(package_file.reload).to be_error end end end diff --git a/yarn.lock b/yarn.lock index 76aaeb3e6f5..ee08337f3b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1016,10 +1016,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== -"@gitlab/eslint-plugin@13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-13.0.0.tgz#fa8d0ad96cfaeaa42f2d978ff059fc17358a00f8" - integrity sha512-w7vhBiSMslam1IPeprc2cArrLW6GqIFW9cW/CEwbim8dmzT8wZFzLvTSnIHQdokPN4fM5aToCLr9HkCROy4cRg== +"@gitlab/eslint-plugin@13.1.0": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-13.1.0.tgz#d0698251e601d8732b6db994c8ebd8c37be404fa" + integrity sha512-/eWbTomAipyp/nbaNLq8aU1IcqG029+clvUOgkckm704q38G0/r6KHgcuXvxWj2erzwcEveEXXyilZAaTQquRA== dependencies: "@babel/core" "^7.17.0" "@babel/eslint-parser" "^7.17.0" @@ -1072,10 +1072,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== -"@graphql-eslint/eslint-plugin@3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.4.tgz#fcc3b54ef1d6d38e89daf848d93be2d5359fef98" - integrity sha512-+Vmi1E5uSWOgtylosHS3HYWHF+x0GMgaMOHmSfOflTE51VXnLM2tvGKMd3lpyngCEZG1wjvqdlB5rn2iwhhzqA== +"@graphql-eslint/eslint-plugin@3.10.5": + version "3.10.5" + resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.5.tgz#a5d26fe95b52d5fbd02a4c122ca0bc1b2d62fcb2" + integrity sha512-rMsuoXA9ldD5IU+3sv9BqDb9SmP+BJFtzF8Y4bV1Pj5O3SROkVDHk/dbN4pv5uFu+Az/AM1BkwVbSzz9CvP5Sw== dependencies: "@babel/code-frame" "^7.16.7" "@graphql-tools/code-file-loader" "^7.2.14" |