diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-30 18:08:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-30 18:08:03 +0300 |
commit | 02c48d0a6bf00afd66a603253ec59db4e1412392 (patch) | |
tree | 0b7ea896f1f535e55e1988f4cb8d105e3983ea7c | |
parent | f1284938edfc2e033baf2c26ebadf42c526f6432 (diff) |
Add latest changes from gitlab-org/gitlab@master
70 files changed, 1261 insertions, 148 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 83f42982dab..87f011aa132 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -175,10 +175,8 @@ - ".gitlab/ci/workhorse.gitlab-ci.yml" .yaml-lint-patterns: &yaml-lint-patterns - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*.yml" - - "data/**/*.yml" - - "lib/gitlab/ci/templates/**/*.yml" + - "*.yml" + - "**/*.yml" .docs-patterns: &docs-patterns - ".gitlab/route-map.yml" diff --git a/.gitlab/ci/yaml.gitlab-ci.yml b/.gitlab/ci/yaml.gitlab-ci.yml index ac32e4226e2..a0665d712dc 100644 --- a/.gitlab/ci/yaml.gitlab-ci.yml +++ b/.gitlab/ci/yaml.gitlab-ci.yml @@ -1,4 +1,5 @@ -# Yamllint of CI-related yaml. +# Yamllint of yaml files. + # This uses rules from project root `.yamllint`. lint-yaml: extends: @@ -7,10 +8,24 @@ lint-yaml: image: pipelinecomponents/yamllint:latest stage: lint needs: [] + script: + - yamllint --strict -f colored . + +# The jobs below will not use the configuration present in `.yamllint` (it's because of the -d option) +# +# Docs: https://yamllint.readthedocs.io/en/stable/configuration.html#custom-configuration-without-a-config-file + +lint-pipeline-yaml: + extends: + - .default-retry + - .yaml-lint:rules + image: pipelinecomponents/yamllint:latest + stage: lint + needs: [] variables: LINT_PATHS: .gitlab-ci.yml .gitlab/ci lib/gitlab/ci/templates data/deprecations data/removals data/whats_new script: - - yamllint --strict -f colored $LINT_PATHS + - 'yamllint -d "{extends: default, rules: {line-length: disable, document-start: disable}}" $LINT_PATHS' lint-metrics-yaml: extends: diff --git a/.yamllint b/.yamllint index df7cdf404bc..2fddf9ee3c4 100644 --- a/.yamllint +++ b/.yamllint @@ -2,6 +2,44 @@ extends: default +# Ideally, we should have nothing in this ignore section. +# +# Please consider removing entries below by fixing them. +ignore: | + #### Files #### + + # Contains ruby code + config/mail_room.yml + generator_templates/snowplow_event_definition/event_definition.yml + generator_templates/usage_metric_definition/metric_definition.yml + + # Has some special indentation + doc/user/project/integrations/samples/cloudwatch.yml + + # Broken on purpose (for testing) + spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml + + #### Folders #### + node_modules/ + tmp/ + +# Why disabling all of those rules? +# +# For the scope of https://gitlab.com/gitlab-org/gitlab/-/issues/359968, +# we would like to catch syntax errors as soon as possible. +# Style "errors" are not as important right now, but they should ideally be added later on. +# +# Please consider enabling a rule, and fixing the issues you'll see in an MR. rules: - line-length: disable + braces: disable + colons: disable + comments-indentation: disable + comments: disable document-start: disable + empty-lines: disable + indentation: disable + key-duplicates: disable + line-length: disable + new-line-at-end-of-file: disable + trailing-spaces: disable + truthy: disable diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue index 6ac1bce4032..567e534d9cf 100644 --- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue +++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue @@ -1,5 +1,5 @@ <script> -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoader } from '@gitlab/ui'; import { flatten, isEqual, keyBy } from 'lodash'; import createFlash from '~/flash'; import { sprintf, s__ } from '~/locale'; @@ -48,7 +48,7 @@ const groupRawMetrics = (groups = [], rawData = []) => { export default { name: 'ValueStreamMetrics', components: { - GlSkeletonLoading, + GlSkeletonLoader, MetricTile, }, props: { @@ -119,8 +119,8 @@ export default { }; </script> <template> - <div class="gl-display-flex gl-mt-6" data-testid="vsa-metrics"> - <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" /> + <div class="gl-display-flex" data-testid="vsa-metrics" :class="isLoading ? 'gl-my-6' : 'gl-mt-6'"> + <gl-skeleton-loader v-if="isLoading" /> <template v-else> <div v-if="hasGroupedMetrics" class="gl-flex-direction-column"> <div diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 2c42a331550..871cbaf0d1a 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -53,7 +53,7 @@ export default { primaryProps() { return { text: __('Leave group'), - attributes: [{ variant: 'warning' }, { category: 'primary' }], + attributes: [{ variant: 'danger' }, { category: 'primary' }], }; }, cancelProps() { diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 403e216e70f..c570f8810a8 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,7 +1,5 @@ -import $ from 'jquery'; - export default function initLogoAnimation() { window.addEventListener('beforeunload', () => { - $('.tanuki-logo').addClass('animate'); + document.querySelector('.tanuki-logo').classList.add('animate'); }); } diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 690295e6068..49fdf5bb6b5 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -6,7 +6,7 @@ const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSkippable) : false; if (skippable) { - const button = `<br/><a class="btn btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 03bab0fa773..d598941bade 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -2,6 +2,7 @@ import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __, s__ } from '~/locale'; import { visibilityOptions, @@ -16,7 +17,7 @@ import { toggleHiddenClassBySelector } from '../external'; import projectFeatureSetting from './project_feature_setting.vue'; import projectSettingRow from './project_setting_row.vue'; -const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone'); +const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')]; export default { i18n: { @@ -28,7 +29,14 @@ export default { lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'), mergeRequestsLabel: s__('ProjectSettings|Merge requests'), operationsLabel: s__('ProjectSettings|Operations'), + packagesHelpText: s__( + 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.', + ), + packageRegistryHelpText: s__( + 'ProjectSettings|Every project can have its own space to store its packages.', + ), packagesLabel: s__('ProjectSettings|Packages'), + packageRegistryLabel: s__('ProjectSettings|Package registry'), pagesLabel: s__('ProjectSettings|Pages'), ciCdLabel: __('CI/CD'), repositoryLabel: s__('ProjectSettings|Repository'), @@ -54,7 +62,7 @@ export default { GlToggle, ConfirmDanger, }, - mixins: [settingsMixin], + mixins: [settingsMixin, glFeatureFlagsMixin()], props: { requestCveAvailable: { @@ -183,6 +191,7 @@ export default { repositoryAccessLevel: featureAccessLevel.EVERYONE, forkingAccessLevel: featureAccessLevel.EVERYONE, mergeRequestsAccessLevel: featureAccessLevel.EVERYONE, + packageRegistryAccessLevel: featureAccessLevel.EVERYONE, buildsAccessLevel: featureAccessLevel.EVERYONE, wikiAccessLevel: featureAccessLevel.EVERYONE, snippetsAccessLevel: featureAccessLevel.EVERYONE, @@ -229,6 +238,18 @@ export default { ); }, + packageRegistryFeatureAccessLevelOptions() { + const options = [FEATURE_ACCESS_LEVEL_ANONYMOUS]; + + if (this.visibilityLevel === visibilityOptions.PRIVATE) { + options.unshift(featureAccessLevelMembers); + } else if (this.visibilityLevel === visibilityOptions.INTERNAL) { + options.unshift(featureAccessLevelEveryone); + } + + return options; + }, + pagesFeatureAccessLevelOptions() { const options = [featureAccessLevelMembers]; @@ -242,7 +263,7 @@ export default { } if (this.visibilityLevel !== visibilityOptions.PUBLIC) { - options.push([30, PAGE_FEATURE_ACCESS_LEVEL]); + options.push(FEATURE_ACCESS_LEVEL_ANONYMOUS); } } return options; @@ -285,6 +306,9 @@ export default { this.visibilityLevel < this.currentSettings.visibilityLevel ); }, + packageRegistryAccessLevelEnabled() { + return this.glFeatures.packageRegistryAccessLevel; + }, }, watch: { @@ -307,6 +331,15 @@ export default { featureAccessLevel.PROJECT_MEMBERS, this.buildsAccessLevel, ); + if (this.packageRegistryAccessLevelEnabled) { + if ( + this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE || + (this.packageRegistryAccessLevel > featureAccessLevel.EVERYONE && + oldValue === visibilityOptions.PUBLIC) + ) { + this.packageRegistryAccessLevel = featureAccessLevel.PROJECT_MEMBERS; + } + } this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel); this.snippetsAccessLevel = Math.min( featureAccessLevel.PROJECT_MEMBERS, @@ -349,6 +382,14 @@ export default { this.repositoryAccessLevel = featureAccessLevel.EVERYONE; if (this.mergeRequestsAccessLevel > featureAccessLevel.NOT_ENABLED) this.mergeRequestsAccessLevel = featureAccessLevel.EVERYONE; + if ( + this.packageRegistryAccessLevelEnabled && + this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS + ) { + this.packageRegistryAccessLevel = Math.min( + ...this.packageRegistryFeatureAccessLevelOptions.map((option) => option[0]), + ); + } if (this.buildsAccessLevel > featureAccessLevel.NOT_ENABLED) this.buildsAccessLevel = featureAccessLevel.EVERYONE; if (this.wikiAccessLevel > featureAccessLevel.NOT_ENABLED) @@ -369,6 +410,19 @@ export default { this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE; this.highlightChanges(); + } else if (this.packageRegistryAccessLevelEnabled) { + if ( + value === visibilityOptions.PUBLIC && + this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE + ) { + // eslint-disable-next-line prefer-destructuring + this.packageRegistryAccessLevel = FEATURE_ACCESS_LEVEL_ANONYMOUS[0]; + } else if ( + value === visibilityOptions.INTERNAL && + this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0] + ) { + this.packageRegistryAccessLevel = featureAccessLevel.EVERYONE; + } } }, @@ -587,15 +641,11 @@ export default { </p> </project-setting-row> <project-setting-row - v-if="packagesAvailable" + v-if="packagesAvailable && !packageRegistryAccessLevelEnabled" ref="package-settings" :help-path="packagesHelpPath" :label="$options.i18n.packagesLabel" - :help-text=" - s__( - 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.', - ) - " + :help-text="$options.i18n.packagesHelpText" > <gl-toggle v-model="packagesEnabled" @@ -710,6 +760,20 @@ export default { /> </project-setting-row> <project-setting-row + v-if="packageRegistryAccessLevelEnabled && packagesAvailable" + :help-path="packagesHelpPath" + :label="$options.i18n.packageRegistryLabel" + :help-text="$options.i18n.packageRegistryHelpText" + data-testid="package-registry-access-level" + > + <project-feature-setting + v-model="packageRegistryAccessLevel" + :label="$options.i18n.packageRegistryLabel" + :options="packageRegistryFeatureAccessLevelOptions" + name="project[project_feature_attributes][package_registry_access_level]" + /> + </project-setting-row> + <project-setting-row v-if="pagesAvailable && pagesAccessControlEnabled" ref="pages-settings" :help-path="pagesHelpPath" diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue index 897bd2dcccf..1f74e89f90c 100644 --- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue +++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue @@ -1,6 +1,8 @@ <script> import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; +import Tracking from '~/tracking'; +import { pipelineEditorTrackingOptions } from '../../../constants'; export default { i18n: { @@ -25,7 +27,14 @@ export default { GlLink, GlSprintf, }, + mixins: [Tracking.mixin()], inject: ['runnerHelpPagePath'], + methods: { + trackHelpPageClick() { + const { label, actions } = pipelineEditorTrackingOptions; + this.track(actions.helpDrawerLinks.runners, { label }); + }, + }, }; </script> <template> @@ -38,7 +47,7 @@ export default { <p class="gl-mb-0"> <gl-sprintf :message="$options.i18n.note"> <template #link="{ content }"> - <gl-link :href="runnerHelpPagePath" target="_blank"> + <gl-link :href="runnerHelpPagePath" target="_blank" @click="trackHelpPageClick()"> {{ content }} </gl-link> </template> diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue index 04140434af2..bc9203b9c5b 100644 --- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue +++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue @@ -1,8 +1,20 @@ <script> import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; +import Tracking from '~/tracking'; +import { + CI_EXAMPLES_LINK, + CI_HELP_LINK, + CI_NEEDS_LINK, + CI_YAML_LINK, + pipelineEditorTrackingOptions, +} from '../../../constants'; export default { + CI_EXAMPLES_LINK, + CI_HELP_LINK, + CI_NEEDS_LINK, + CI_YAML_LINK, i18n: { title: s__('PipelineEditorTutorial|⚙️ Pipeline configuration reference'), firstParagraph: s__('PipelineEditorTutorial|Resources to help with your CI/CD configuration:'), @@ -23,7 +35,14 @@ export default { GlLink, GlSprintf, }, + mixins: [Tracking.mixin()], inject: ['ciExamplesHelpPagePath', 'ciHelpPagePath', 'needsHelpPagePath', 'ymlHelpPagePath'], + methods: { + trackHelpPageClick(key) { + const { label, actions } = pipelineEditorTrackingOptions; + this.track(actions.helpDrawerLinks[key], { label }); + }, + }, }; </script> <template> @@ -34,7 +53,11 @@ export default { <li> <gl-sprintf :message="$options.i18n.browseExamples"> <template #link="{ content }"> - <gl-link :href="ciExamplesHelpPagePath" target="_blank"> + <gl-link + :href="ciExamplesHelpPagePath" + target="_blank" + @click="trackHelpPageClick($options.CI_EXAMPLES_LINK)" + > {{ content }} </gl-link> </template> @@ -43,7 +66,11 @@ export default { <li> <gl-sprintf :message="$options.i18n.viewSyntaxRef"> <template #link="{ content }"> - <gl-link :href="ymlHelpPagePath" target="_blank"> + <gl-link + :href="ymlHelpPagePath" + target="_blank" + @click="trackHelpPageClick($options.CI_YAML_LINK)" + > {{ content }} </gl-link> </template> @@ -52,7 +79,11 @@ export default { <li> <gl-sprintf :message="$options.i18n.learnMore"> <template #link="{ content }"> - <gl-link :href="ciHelpPagePath" target="_blank"> + <gl-link + :href="ciHelpPagePath" + target="_blank" + @click="trackHelpPageClick($options.CI_HELP_LINK)" + > {{ content }} </gl-link> </template> @@ -61,7 +92,11 @@ export default { <li> <gl-sprintf :message="$options.i18n.needs"> <template #link="{ content }"> - <gl-link :href="needsHelpPagePath" target="_blank"> + <gl-link + :href="needsHelpPagePath" + target="_blank" + @click="trackHelpPageClick($options.CI_NEEDS_LINK)" + > {{ content }} </gl-link> </template> diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue index 9765d669fc1..65a2a6b56e4 100644 --- a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue +++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue @@ -22,12 +22,21 @@ export default { }, methods: { toggleDrawer() { - this.$emit(this.showDrawer ? 'close-drawer' : 'open-drawer'); + if (this.showDrawer) { + this.$emit('close-drawer'); + } else { + this.$emit('open-drawer'); + this.trackHelpDrawerClick(); + } + }, + trackHelpDrawerClick() { + const { label, actions } = pipelineEditorTrackingOptions; + this.track(actions.openHelpDrawer, { label }); }, trackTemplateBrowsing() { const { label, actions } = pipelineEditorTrackingOptions; - this.track(actions.browse_templates, { label }); + this.track(actions.browseTemplates, { label }); }, }, }; diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index ff7c742f588..04fe50e5f09 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -55,10 +55,25 @@ export const FILE_TREE_TIP_DISMISSED_KEY = 'pipeline_editor_file_tree_tip_dismis export const STARTER_TEMPLATE_NAME = 'Getting-Started'; +export const CI_EXAMPLES_LINK = 'CI_EXAMPLES_LINK'; +export const CI_HELP_LINK = 'CI_HELP_LINK'; +export const CI_NEEDS_LINK = 'CI_NEEDS_LINK'; +export const CI_RUNNERS_LINK = 'CI_RUNNERS_LINK'; +export const CI_YAML_LINK = 'CI_YAML_LINK'; + export const pipelineEditorTrackingOptions = { label: 'pipeline_editor', actions: { - browse_templates: 'browse_templates', + browseTemplates: 'browse_templates', + closeHelpDrawer: 'close_help_drawer', + helpDrawerLinks: { + [CI_EXAMPLES_LINK]: 'visit_help_drawer_link_ci_examples', + [CI_HELP_LINK]: 'visit_help_drawer_link_ci_help', + [CI_NEEDS_LINK]: 'visit_help_drawer_link_needs', + [CI_RUNNERS_LINK]: 'visit_help_drawer_link_runners', + [CI_YAML_LINK]: 'visit_help_drawer_link_yaml', + }, + openHelpDrawer: 'open_help_drawer', }, }; diff --git a/app/assets/javascripts/pipeline_wizard/templates/.gitkeep b/app/assets/javascripts/pipeline_wizard/templates/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/templates/.gitkeep diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c363bf5021b..6dd93e156c4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -43,6 +43,7 @@ class ProjectsController < Projects::ApplicationController push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies) push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?) push_frontend_feature_flag(:work_item_assignees) + push_frontend_feature_flag(:package_registry_access_level) end layout :determine_layout @@ -411,6 +412,7 @@ class ProjectsController < Projects::ApplicationController repository_access_level snippets_access_level wiki_access_level + package_registry_access_level pages_access_level metrics_dashboard_access_level analytics_access_level diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c3f22dc7693..6f57961d888 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -596,6 +596,7 @@ module ProjectsHelper feature = project.project_feature { packagesEnabled: !!project.packages_enabled, + packageRegistryAccessLevel: feature.package_registry_access_level, visibilityLevel: project.visibility_level, requestAccessEnabled: !!project.request_access_enabled, issuesAccessLevel: feature.issues_access_level, diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 74234a4e470..67be6b4f8ee 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -140,8 +140,9 @@ module Ci scope :for_job_ids, ->(job_ids) { where(job_id: job_ids) } scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) } scope :created_at_before, ->(time) { where(arel_table[:created_at].lteq(time)) } + scope :id_before, ->(id) { where(arel_table[:id].lteq(id)) } scope :id_after, ->(id) { where(arel_table[:id].gt(id)) } - scope :ordered_by_created_at_and_id_asc, -> { order(:created_at, :id) } + scope :ordered_by_id, -> { order(:id) } scope :with_job, -> { joins(:job).includes(:job) } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3bd6959b99f..74d7f9788c7 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -81,6 +81,7 @@ module Ci has_many :downloadable_artifacts, -> do not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job end, through: :latest_builds, source: :job_artifacts + has_many :latest_successful_builds, -> { latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 0cab874a240..900e8f7d39b 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -66,6 +66,10 @@ module ProjectFeaturesCompatibility write_feature_attribute_string(:snippets_access_level, value) end + def package_registry_access_level=(value) + write_feature_attribute_string(:package_registry_access_level, value) + end + def pages_access_level=(value) write_feature_attribute_string(:pages_access_level, value) end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 863e8bcfaa8..68df6f0f215 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -182,6 +182,38 @@ class Deployment < ApplicationRecord find(ids) end + # This method returns the deployment records of the last deployment pipeline, that successfully executed for the given environment. + # e.g. + # A pipeline contains + # - deploy job A => production environment + # - deploy job B => production environment + # In this case, `last_deployment_group` returns both deployments. + # + # NOTE: Preload environment.last_deployment and pipeline.latest_successful_builds prior to avoid N+1. + def self.last_deployment_group_for_environment(env) + return self.none unless env.last_deployment_pipeline&.latest_successful_builds&.present? + + BatchLoader.for(env).batch do |environments, loader| + latest_successful_build_ids = [] + environments_hash = {} + + environments.each do |environment| + environments_hash[environment.id] = environment + + # Refer comment note above, if not preloaded this can lead to N+1. + latest_successful_build_ids << environment.last_deployment_pipeline.latest_successful_builds.map(&:id) + end + + Deployment + .where(deployable_type: 'CommitStatus', deployable_id: latest_successful_build_ids.flatten) + .preload(last_deployment_group_associations) + .group_by { |deployment| deployment.environment_id } + .each do |env_id, deployment_group| + loader.call(environments_hash[env_id], deployment_group) + end + end + end + def self.distinct_on_environment order('environment_id, deployments.id DESC') .select('DISTINCT ON (environment_id) deployments.*') @@ -439,6 +471,18 @@ class Deployment < ApplicationRecord raise ArgumentError, "The status #{status.inspect} is invalid" end end + + def self.last_deployment_group_associations + { + deployable: { + pipeline: { + manual_actions: [] + } + } + } + end + + private_class_method :last_deployment_group_associations end Deployment.prepend_mod_with('Deployment') diff --git a/app/models/environment.rb b/app/models/environment.rb index 6b7417ee24f..f3453bc5844 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -208,7 +208,7 @@ class Environment < ApplicationRecord # - deploy job A => production environment # - deploy job B => production environment # In this case, `last_deployment_group` returns both deployments, whereas `last_deployable` returns only B. - def last_deployment_group + def legacy_last_deployment_group return Deployment.none unless last_deployment_pipeline successful_deployments.where( @@ -324,12 +324,18 @@ class Environment < ApplicationRecord def stop_actions strong_memoize(:stop_actions) do - # Fix N+1 queries it brings to the serializer. - # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780 last_deployment_group.map(&:stop_action).compact end end + def last_deployment_group + if ::Feature.enabled?(:batch_load_environment_last_deployment_group, project) + Deployment.last_deployment_group_for_environment(self) + else + legacy_last_deployment_group + end + end + def reset_auto_stop update_column(:auto_stop_at, nil) end diff --git a/app/models/project.rb b/app/models/project.rb index 759e3d348fb..16b85a6835e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -121,6 +121,8 @@ class Project < ApplicationRecord before_save :ensure_runners_token before_validation :ensure_project_namespace_in_sync + before_validation :set_package_registry_access_level, if: :packages_enabled_changed? + after_save :update_project_statistics, if: :saved_change_to_namespace_id? after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? } @@ -445,7 +447,7 @@ class Project < ApplicationRecord :pages_enabled?, :analytics_enabled?, :snippets_enabled?, :public_pages?, :private_pages?, :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, - :repository_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, + :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_enabled?, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :container_registry_enabled?, to: :project_feature, allow_nil: true @@ -3150,6 +3152,23 @@ class Project < ApplicationRecord raise ExportLimitExceeded, _('The project size exceeds the export limit.') end end + + def set_package_registry_access_level + return if !project_feature || project_feature.package_registry_access_level_changed? + + self.project_feature.package_registry_access_level = packages_enabled ? enabled_package_registry_access_level_by_project_visibility : ProjectFeature::DISABLED + end + + def enabled_package_registry_access_level_by_project_visibility + case visibility_level + when PUBLIC + ProjectFeature::PUBLIC + when INTERNAL + ProjectFeature::ENABLED + else + ProjectFeature::PRIVATE + end + end end Project.prepend_mod_with('Project') diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 27692fe76f0..f478af32788 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -20,6 +20,7 @@ class ProjectFeature < ApplicationRecord operations security_and_compliance container_registry + package_registry ].freeze EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze @@ -29,7 +30,8 @@ class ProjectFeature < ApplicationRecord PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER, - container_registry: Gitlab::Access::REPORTER + container_registry: Gitlab::Access::REPORTER, + package_registry: Gitlab::Access::REPORTER }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze @@ -76,6 +78,14 @@ class ProjectFeature < ApplicationRecord end end + default_value_for(:package_registry_access_level) do |feature| + if ::Gitlab.config.packages.enabled + ENABLED + else + DISABLED + end + end + default_value_for(:container_registry_access_level) do |feature| if gitlab_config_features.container_registry ENABLED @@ -142,6 +152,12 @@ class ProjectFeature < ApplicationRecord !public_pages? end + def package_registry_access_level=(value) + super(value).tap do + project.packages_enabled = self.package_registry_access_level != DISABLED if project + end + end + private # Validates builds and merge requests access level @@ -157,7 +173,7 @@ class ProjectFeature < ApplicationRecord end def feature_validation_exclusion - %i(pages) + %i(pages package_registry) end override :resource_member? diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb index f411dbe88ed..c92898068c3 100644 --- a/app/models/projects/build_artifacts_size_refresh.rb +++ b/app/models/projects/build_artifacts_size_refresh.rb @@ -36,6 +36,7 @@ module Projects before_transition created: :running do |refresh| refresh.reset_project_statistics! refresh.refresh_started_at = Time.zone.now + refresh.last_job_artifact_id_on_refresh_start = refresh.project.job_artifacts.last&.id end before_transition running: any do |refresh, transition| @@ -83,9 +84,9 @@ module Projects def next_batch(limit:) project.job_artifacts.select(:id, :size) - .created_at_before(refresh_started_at) + .id_before(last_job_artifact_id_on_refresh_start) .id_after(last_job_artifact_id.to_i) - .ordered_by_created_at_and_id_asc + .ordered_by_id .limit(limit) end diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 3cb60a59f51..3f236fa55df 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -52,19 +52,30 @@ class EnvironmentSerializer < BaseSerializer end def batch_load(resource) + temp_deployment_associations = deployment_associations + resource = resource.preload(environment_associations.except(:last_deployment, :upcoming_deployment)) + if ::Feature.enabled?(:batch_load_environment_last_deployment_group, resource.first&.project) + temp_deployment_associations[:deployable][:pipeline][:latest_successful_builds] = [] + end + Preloaders::Environments::DeploymentPreloader.new(resource) - .execute_with_union(:last_deployment, deployment_associations) + .execute_with_union(:last_deployment, temp_deployment_associations) Preloaders::Environments::DeploymentPreloader.new(resource) - .execute_with_union(:upcoming_deployment, deployment_associations) + .execute_with_union(:upcoming_deployment, temp_deployment_associations) resource.to_a.tap do |environments| environments.each do |environment| # Batch loading the commits of the deployments environment.last_deployment&.commit&.try(:lazy_author) environment.upcoming_deployment&.commit&.try(:lazy_author) + + if ::Feature.enabled?(:batch_load_environment_last_deployment_group, environment.project) + # Batch loading last_deployment_group which is called later by environment.stop_actions + environment.last_deployment_group + end end end end @@ -89,10 +100,11 @@ class EnvironmentSerializer < BaseSerializer user: [], metadata: [], pipeline: { - manual_actions: [:metadata], + manual_actions: [:metadata, :deployment], scheduled_actions: [:metadata] }, - project: project_associations + project: project_associations, + deployment: [] } } end diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index f7ffe288d57..316e6367aa7 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -12,13 +12,15 @@ module ResourceAccessTokens def execute return error("User does not have permission to create #{resource_type} access token") unless has_permission_to_create? + access_level = params[:access_level] || Gitlab::Access::MAINTAINER + return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level) + user = create_user return error(user.errors.full_messages.to_sentence) unless user.persisted? user.update!(external: true) if current_user.external? - access_level = params[:access_level] || Gitlab::Access::MAINTAINER member = create_membership(resource, user, access_level) unless member.persisted? @@ -120,6 +122,12 @@ module ResourceAccessTokens def success(access_token) ServiceResponse.success(payload: { access_token: access_token }) end + + def do_not_allow_owner_access_level_for_project_bot?(access_level) + resource.is_a?(Project) && + access_level == Gitlab::Access::OWNER && + !current_user.can?(:manage_owners, resource) + end end end diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 8c799a5e3fe..37fdba141ee 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,6 +1,7 @@ - page_title _('Preferences') - @content_class = "limit-container-width" unless fluid_layout - user_theme_id = Gitlab::Themes.for_user(@user).id +- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id - user_fields = { theme: user_theme_id, gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }.to_json - @themes = Gitlab::Themes::available_themes.to_json - data_attributes = { themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path } @@ -20,8 +21,9 @@ - Gitlab::Themes.each do |theme| %label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center .preview{ class: theme.css_class } - = f.radio_button :theme_id, theme.id, checked: user_theme_id == theme.id - = theme.name + = f.gitlab_ui_radio_component :theme_id, theme.id, + theme.name, + radio_options: { checked: user_theme_id == theme.id } .col-sm-12 %hr @@ -38,8 +40,9 @@ - Gitlab::ColorSchemes.each do |scheme| = label_tag do .preview= image_tag "#{scheme.css_class}-scheme-preview.png" - = f.radio_button :color_scheme_id, scheme.id - = scheme.name + = f.gitlab_ui_radio_component :color_scheme_id, scheme.id, + scheme.name, + radio_options: { checked: user_color_schema_id == scheme.id } .col-sm-12 %hr diff --git a/config/feature_flags/development/batch_load_environment_last_deployment_group.yml b/config/feature_flags/development/batch_load_environment_last_deployment_group.yml new file mode 100644 index 00000000000..d6de45eacdf --- /dev/null +++ b/config/feature_flags/development/batch_load_environment_last_deployment_group.yml @@ -0,0 +1,8 @@ +--- +name: batch_load_environment_last_deployment_group +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86584/ +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363023 +milestone: '15.1' +type: development +group: group::release +default_enabled: false diff --git a/config/feature_flags/development/package_registry_access_level.yml b/config/feature_flags/development/package_registry_access_level.yml new file mode 100644 index 00000000000..093315f77bd --- /dev/null +++ b/config/feature_flags/development/package_registry_access_level.yml @@ -0,0 +1,8 @@ +--- +name: package_registry_access_level +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82808 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363018 +milestone: '15.0' +type: development +group: group::package +default_enabled: false diff --git a/db/migrate/20220314094841_add_package_registry_access_level_into_project_features.rb b/db/migrate/20220314094841_add_package_registry_access_level_into_project_features.rb new file mode 100644 index 00000000000..4effdfa6a5e --- /dev/null +++ b/db/migrate/20220314094841_add_package_registry_access_level_into_project_features.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddPackageRegistryAccessLevelIntoProjectFeatures < Gitlab::Database::Migration[1.0] + DISABLED = 0 # ProjectFeature::DISABLED + + def up + add_column :project_features, :package_registry_access_level, :integer, default: DISABLED, null: false + end + + def down + remove_column :project_features, :package_registry_access_level + end +end diff --git a/db/migrate/20220519141345_add_job_artifact_id_on_refresh_start_to_build_artifacts_size_refresh.rb b/db/migrate/20220519141345_add_job_artifact_id_on_refresh_start_to_build_artifacts_size_refresh.rb new file mode 100644 index 00000000000..ee19fd9da23 --- /dev/null +++ b/db/migrate/20220519141345_add_job_artifact_id_on_refresh_start_to_build_artifacts_size_refresh.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddJobArtifactIdOnRefreshStartToBuildArtifactsSizeRefresh < Gitlab::Database::Migration[2.0] + def change + add_column :project_build_artifacts_size_refreshes, :last_job_artifact_id_on_refresh_start, :bigint, default: 0 + end +end diff --git a/db/schema_migrations/20220314094841 b/db/schema_migrations/20220314094841 new file mode 100644 index 00000000000..f214ecaa9b3 --- /dev/null +++ b/db/schema_migrations/20220314094841 @@ -0,0 +1 @@ +eabdb1e45a67947401963a971f24ae1b19068c72f5d4dd61d7fd47b8e61f1ed2
\ No newline at end of file diff --git a/db/schema_migrations/20220519141345 b/db/schema_migrations/20220519141345 new file mode 100644 index 00000000000..45b67d9cc53 --- /dev/null +++ b/db/schema_migrations/20220519141345 @@ -0,0 +1 @@ +ff20989814782030135a9c55831896b89275293f04d7edbb4a5b6ae568d2a455
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ef434682dba..007bc05290d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19150,7 +19150,8 @@ CREATE TABLE project_build_artifacts_size_refreshes ( state smallint DEFAULT 1 NOT NULL, refresh_started_at timestamp with time zone, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + last_job_artifact_id_on_refresh_start bigint DEFAULT 0 ); CREATE SEQUENCE project_build_artifacts_size_refreshes_id_seq @@ -19321,7 +19322,8 @@ CREATE TABLE project_features ( operations_access_level integer DEFAULT 20 NOT NULL, analytics_access_level integer DEFAULT 20 NOT NULL, security_and_compliance_access_level integer DEFAULT 10 NOT NULL, - container_registry_access_level integer DEFAULT 0 NOT NULL + container_registry_access_level integer DEFAULT 0 NOT NULL, + package_registry_access_level integer DEFAULT 0 NOT NULL ); CREATE SEQUENCE project_features_id_seq diff --git a/doc/administration/monitoring/ip_whitelist.md b/doc/administration/monitoring/ip_whitelist.md index b8347ba3f0d..adf9516733a 100644 --- a/doc/administration/monitoring/ip_whitelist.md +++ b/doc/administration/monitoring/ip_whitelist.md @@ -1,6 +1,6 @@ --- -stage: Monitor -group: Respond +stage: Data Stores +group: Memory 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 --- diff --git a/doc/api/project_access_tokens.md b/doc/api/project_access_tokens.md index b13005ec436..fa66c8950d3 100644 --- a/doc/api/project_access_tokens.md +++ b/doc/api/project_access_tokens.md @@ -87,6 +87,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a Create a [project access token](../user/project/settings/project_access_tokens.md). +**NOTE:** Project Maintainers cannot create project access tokens with Owner (50) access level. + ```plaintext POST projects/:id/access_tokens ``` diff --git a/doc/development/cicd/img/pipeline_wizard_sample_step1.png b/doc/development/cicd/img/pipeline_wizard_sample_step1.png Binary files differnew file mode 100644 index 00000000000..77e5f07aad2 --- /dev/null +++ b/doc/development/cicd/img/pipeline_wizard_sample_step1.png diff --git a/doc/development/cicd/img/pipeline_wizard_sample_step2.png b/doc/development/cicd/img/pipeline_wizard_sample_step2.png Binary files differnew file mode 100644 index 00000000000..b59414d78f9 --- /dev/null +++ b/doc/development/cicd/img/pipeline_wizard_sample_step2.png diff --git a/doc/development/cicd/img/pipeline_wizard_sample_step3.png b/doc/development/cicd/img/pipeline_wizard_sample_step3.png Binary files differnew file mode 100644 index 00000000000..42c995b64d6 --- /dev/null +++ b/doc/development/cicd/img/pipeline_wizard_sample_step3.png diff --git a/doc/development/cicd/pipeline_wizard.md b/doc/development/cicd/pipeline_wizard.md new file mode 100644 index 00000000000..f949386ba98 --- /dev/null +++ b/doc/development/cicd/pipeline_wizard.md @@ -0,0 +1,229 @@ +--- +stage: none +group: Incubation Engineering +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 +--- + +# Pipeline Wizard + +The Pipeline Wizard is a Vue frontend component that helps users create a +pipeline by using input fields. The type of input fields and the form of the final +pipeline is configured by a YAML template. + +The Pipeline Wizard expects a single template file that configures the user +flow. The wizard is agnostic with regards to the contents of the file, +so you can use the wizard to display a range of different flows. For example, there +could be one template file for static sites, +one for Docker images, one for mobile apps, and so on. As a first iteration, +these templates are part of the GitLab source code. + +The template file defines multiple steps. The last step shown to the user is always +the commit, and is not part of the template definition. An ideal user experience +consists of 2-3 steps, for a total of 3-4 steps visible to the user. + +## Usage Example + +### Vue Component + +```vue +<!-- ~/my_feature/my_component.vue --> + +<script> + import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue' + import template from '~/pipeline_wizard/templates/my_template.yml'; + + export default { + name: "MyComponent", + components: { PipelineWizard }, + data() { + return { template } + }, + methods: { + onDone() { + // redirect + } + } + } +</script> + +<template> + <pipeline-wizard :template="template" + project-path="foo/bar" + default-branch="main" + @done="onDone" /> +</template> +``` + +### Template + +```yaml +# ~/pipeline_wizard/templates/my_template.yml + +title: Set up my specific tech pipeline +description: Here's two or three introductory sentences that help the user understand what this wizard is going to set up. +steps: + # Step 1 + - inputs: + # First input widget + - label: Select your build image + description: A Docker image that we can use to build your image + placeholder: node:lts + widget: text + target: $BUILD_IMAGE + required: true + pattern: "^(?:(?=[^:\/]{1,253})(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*(?::[0-9]{1,5})?\/)?((?![._-])(?:[a-z0-9._-]*)(?<![._-])(?:\/(?![._-])[a-z0-9._-]*(?<![._-]))*)(?::(?![.-])[a-zA-Z0-9_.-]{1,128})?$" + invalid-feedback: Please enter a valid docker image + + # Second input widget + - label: Installation Steps + description: "Enter the steps that need to run to set up a local build + environment, for example installing dependencies." + placeholder: npm ci + widget: list + target: $INSTALLATION_STEPS + + # This is the template to copy to the final pipeline file and updated with + # the values input by the user. Comments are copied as-is. + template: + my-job: + # The Docker image that will be used to build your app + image: $BUILD_IMAGE + + before_script: $INSTALLATION_STEPS + + artifacts: + paths: + - foo + + # Step 2 + - inputs: + # This is the only input widget for this step + - label: Installation Steps + description: "Enter the steps that need to run to set up a local build + environment, for example installing dependencies." + placeholder: npm ci + widget: list + target: $INSTALLATION_STEPS + + template: + # Functions that should be executed before the build script runs + before_script: $INSTALLATION_STEPS +``` + +### The result + +1. ![Step 1](img/pipeline_wizard_sample_step1.png) +1. ![Step 2](img/pipeline_wizard_sample_step2.png) +1. ![Step 3](img/pipeline_wizard_sample_step3.png) + +### The commit step + +The last step of the wizard is always the commit step. Users can commit the +newly created file to the repository defined by the [wizard's props](#props). +The user has the option to change the branch to commit to. A future iteration +is planned to add the ability to create a MR from here. + +## Component API Reference + +### Props + +- `template` (required): The template content as an unparsed String. See + [Template file location](#template-file-location) for more information. +- `project-path` (required): The full path of the project the final file + should be committed to +- `default-branch` (required): The branch that will be pre-selected during + the commit step. This can be changed by the user. +- `default-filename` (optional, default: `.gitlab-ci.yml`): The Filename + to be used for the file. This can be overridden in the template file. + +### Events + +- `done` - Emitted after the file has been committed. Use this to redirect the +user to the pipeline, for example. + +### Template file location + +Template files are normally stored as YAML files in `~/pipeline_wizard/templates/`. + +The `PipelineWizard` component expects the `template` property as an un-parsed `String`, +and Webpack is configured to load `.yml` files from the above folder as strings. +If you must load the file from a different place, make sure +Webpack does not parse it as an Object. + +## Template Reference + +### Template + +In the root element of the template file, you can define the following properties: + +| Name | Required | Type | Description | +|---------------|------------------------|--------|---------------------------------------------------------------------------------------| +| `title` | **{check-circle}** Yes | string | The page title as displayed to the user. It becomes an `h1` heading above the wizard. | +| `description` | **{check-circle}** Yes | string | The page description as displayed to the user. | +| `filename` | **{dotted-circle}** No | string | The name of the file that is being generated. Defaults to `.gitlab-ci.yml`. | +| `steps` | **{check-circle}** Yes | list | A list of [step definitions](#step-reference). | + +### `step` Reference + +A step makes up one page in a multi-step (or page) process. It consists of one or more +related input fields that build a part of the final `.gitlab-ci.yml`. + +Steps include two properties: + +| Name | Required | Type | Description | +|------------|------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `template` | **{check-circle}** Yes | map | The raw YAML to deep-merge into the final `.gitlab-ci.yml`. This template section can contain variables denoted by a `$` sign that is replaced with the values from the input fields. | +| `inputs` | **{check-circle}** Yes | list | A list of [input definitions](#input-reference). | + +### `input` Reference + +Each step can contain one or more `inputs`. For an ideal user experience, it should not +contain more than three. + +The look and feel of the input, as well as the YAML type it produces (string, list, and so on) +depends on the [`widget`](#widgets) used. [`widget: text`](#text) displays a +text input +and inserts the user's input as a string into the template. [`widget: list`](#list) +displays one or more input fields and inserts a list. + +All `inputs` must have a `label`, `widget`, and optionally `target`, but +most properties +are dependent on the widget being used: + +| Name | Required | Type | Description | +|----------|------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------| +| `label` | **{check-circle}** Yes | string | The label for the input field. | +| `widget` | **{check-circle}** Yes | string | The [widget](#widgets) type to use for this input. | +| `target` | **{dotted-circle}** No | string | The variable name inside the step's template that should be replaced with the value of the input field, for example `$FOO`. | + +### Widgets + +#### Text + +Use as `widget: text`. This inserts a `string` in the YAML file. + +| Name | Required | Type | Description | +|-------------------|------------------------|---------|-----------------------| +| `label` | **{check-circle}** Yes | string | The label for the input field. | +| `description` | **{dotted-circle}** No | string | Help text related to the input field. | +| `required` | **{dotted-circle}** No | boolean | Whether or not the user must provide a value before proceeding to the next step. `false` if not defined. | +| `placeholder` | **{dotted-circle}** No | string | A placeholder for the input field. | +| `pattern` | **{dotted-circle}** No | string | A regular expression that the user's input must match before they can proceed to the next step. | +| `invalidFeedback` | **{dotted-circle}** No | string | Help text displayed when the pattern validation fails. | +| `default` | **{dotted-circle}** No | string | The default value for the field. | +| `id` | **{dotted-circle}** No | string | The input field ID is usually autogenerated but can be overridden by providing this property. | + +#### List + +Use as `widget: list`. This inserts a `list` in the YAML file. + +| Name | Required | Type | Description | +|-------------------|------------------------|---------|-----------------------| +| `label` | **{check-circle}** Yes | string | The label for the input field. | +| `description` | **{dotted-circle}** No | string | Help text related to the input field. | +| `required` | **{dotted-circle}** No | boolean | Whether or not the user must provide a value before proceeding to the next step. `false` if not defined. | +| `placeholder` | **{dotted-circle}** No | string | A placeholder for the input field. | +| `pattern` | **{dotted-circle}** No | string | A regular expression that the user's input must match before they can proceed to the next step. | +| `invalidFeedback` | **{dotted-circle}** No | string | Help text displayed when the pattern validation fails. | +| `default` | **{dotted-circle}** No | list | The default value for the list | +| `id` | **{dotted-circle}** No | string | The input field ID is usually autogenerated but can be overridden by providing this property. | diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 430c0a04a4c..8291d0f5981 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -1708,6 +1708,7 @@ decrypt those columns, preventing access to the following items: - [Project error tracking](../operations/error_tracking.md) - [Runner authentication](../ci/runners/index.md) - [Project mirroring](../user/project/repository/mirror/index.md) +- [Integrations](../user/project/integrations/index.md) - [Web hooks](../user/project/integrations/webhooks.md) In cases like CI/CD variables and runner authentication, you can experience @@ -1882,12 +1883,14 @@ A similar strategy can be employed for the remaining features. By removing the data that can't be decrypted, GitLab can be returned to operation, and the lost data can be manually replaced. -#### Fix project integrations +#### Fix integrations and webhooks -If you've lost your secrets, the [projects' integrations settings pages](../user/project/integrations/index.md) -are probably displaying `500` error messages. +If you've lost your secrets, the [integrations settings pages](../user/project/integrations/index.md) +and [webhooks settings pages](../user/project/integrations/webhooks.md) are probably displaying `500` error messages. -The fix is to truncate the `web_hooks` table: +The fix is to truncate the affected tables (those containing encrypted columns). +This deletes all your configured integrations, webhooks, and related metadata. +You should verify that the secrets are the root cause before deleting any data. 1. Enter the database console: @@ -1915,11 +1918,11 @@ The fix is to truncate the `web_hooks` table: sudo -u git -H bundle exec rails dbconsole -e production --database main ``` -1. Truncate the table: +1. Truncate the following tables: ```sql -- truncate web_hooks table - TRUNCATE web_hooks CASCADE; + TRUNCATE integrations, chat_names, issue_tracker_data, jira_tracker_data, slack_integrations, web_hooks, zentao_tracker_data, web_hook_logs; ``` ### Container Registry push failures after restoring from a backup diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index cf963287a9f..20f84e362cf 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -186,7 +186,7 @@ the default value [is the same as for self-managed instances](../admin_area/sett | Setting | GitLab.com default | |-------------------------------|--------------------| | [Repository size including LFS](../admin_area/settings/account_and_limit_settings.md#repository-size-limit) | 10 GB | -| Maximum import size | 5 GB | +| [Maximum import size](../project/settings/import_export.md#maximum-import-file-size) | 5 GB | | Maximum attachment size | 10 MB | If you are near or over the repository size limit, you can either diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index b2af5f0d294..5c28e38a79e 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -102,6 +102,8 @@ These bot users do not count as licensed seats. The bot users for projects have [permissions](../../permissions.md#project-members-permissions) that correspond with the selected role and [scope](#scopes-for-a-project-access-token) of the project access token. +**Note** Project maintainers cannot select Owner role for bot users. + - The name is set to the name of the token. - The username is set to `project_{project_id}_bot` for the first access token. For example, `project_123_bot`. - The email is set to `project{project_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `project123_bot@noreply.example.com`. diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 1625c39595c..5a1787218f5 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -283,6 +283,7 @@ included_attributes: - :analytics_access_level - :security_and_compliance_access_level - :container_registry_access_level + - :package_registry_access_level prometheus_metrics: - :created_at - :updated_at @@ -684,6 +685,7 @@ included_attributes: - :operations_access_level - :security_and_compliance_access_level - :container_registry_access_level + - :package_registry_access_level - :allow_merge_on_skipped_pipeline - :auto_devops_deploy_strategy - :auto_devops_enabled diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e79d28bc1c2..459fa48dcce 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -29761,6 +29761,9 @@ msgstr "" msgid "ProjectSettings|Every project can have its own space to store its Docker images" msgstr "" +msgid "ProjectSettings|Every project can have its own space to store its packages." +msgstr "" + msgid "ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public." msgstr "" @@ -29878,6 +29881,9 @@ msgstr "" msgid "ProjectSettings|Override user notification preferences for all project members." msgstr "" +msgid "ProjectSettings|Package registry" +msgstr "" + msgid "ProjectSettings|Packages" msgstr "" diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 0a9255e1abe..ccd2011eb8d 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -46,7 +46,7 @@ FactoryBot.define do after(:create) do |environment, evaluator| pipeline = create(:ci_pipeline, project: environment.project) - deployable = create(:ci_build, name: "#{environment.name}:deploy", + deployable = create(:ci_build, :success, name: "#{environment.name}:deploy", pipeline: pipeline) deployment = create(:deployment, diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index a5efadefc76..86321350962 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -29,6 +29,7 @@ FactoryBot.define do merge_requests_access_level { ProjectFeature::ENABLED } repository_access_level { ProjectFeature::ENABLED } analytics_access_level { ProjectFeature::ENABLED } + package_registry_access_level { ProjectFeature::ENABLED } pages_access_level do visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE end @@ -67,6 +68,7 @@ FactoryBot.define do forking_access_level: evaluator.forking_access_level, merge_requests_access_level: merge_requests_access_level, repository_access_level: evaluator.repository_access_level, + package_registry_access_level: evaluator.package_registry_access_level, pages_access_level: evaluator.pages_access_level, metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level, operations_access_level: evaluator.operations_access_level, diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 6c1a338c656..951b24eafac 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -261,6 +261,8 @@ RSpec.describe 'Environment' do context 'when environment is available' do context 'with stop action' do + let(:build) { create(:ci_build, :success, pipeline: pipeline, environment: environment.name) } + let(:action) do create(:ci_build, :manual, pipeline: pipeline, name: 'close_app', diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 23fcc1fe444..649c21d4459 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -189,7 +189,7 @@ RSpec.describe 'Edit Project Settings' do click_button "Save changes" end - expect(find(".sharing-permissions")).to have_selector(".gl-toggle.is-disabled", minimum: 4) + expect(find(".sharing-permissions")).to have_selector(".gl-toggle.is-disabled", minimum: 3) end it "shows empty features project homepage" do diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb index 057e6b635fe..1c2b0faa215 100644 --- a/spec/features/projects/settings/packages_settings_spec.rb +++ b/spec/features/projects/settings/packages_settings_spec.rb @@ -11,6 +11,7 @@ RSpec.describe 'Projects > Settings > Packages', :js do sign_in(user) stub_config(packages: { enabled: packages_enabled }) + stub_feature_flags(package_registry_access_level: package_registry_access_level) visit edit_project_path(project) end @@ -18,14 +19,31 @@ RSpec.describe 'Projects > Settings > Packages', :js do context 'Packages enabled in config' do let(:packages_enabled) { true } - it 'displays the packages toggle button' do - expect(page).to have_selector('[data-testid="toggle-label"]', text: 'Packages') - expect(page).to have_selector('input[name="project[packages_enabled]"] + button', visible: true) + context 'with feature flag disabled' do + let(:package_registry_access_level) { false } + + it 'displays the packages toggle button' do + expect(page).to have_selector('[data-testid="toggle-label"]', text: 'Packages') + expect(page).to have_selector('input[name="project[packages_enabled]"] + button', visible: true) + end + end + + context 'with feature flag enabled' do + let(:package_registry_access_level) { true } + + it 'displays the packages access level setting' do + expect(page).to have_selector('[data-testid="package-registry-access-level"] > label', text: 'Package registry') + expect(page).to have_selector( + 'input[name="project[project_feature_attributes][package_registry_access_level]"]', + visible: false + ) + end end end context 'Packages disabled in config' do let(:packages_enabled) { false } + let(:package_registry_access_level) { false } it 'does not show up in UI' do expect(page).not_to have_selector('[data-testid="toggle-label"]', text: 'Packages') diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js index 4a3e8146b13..df86b10cba3 100644 --- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js +++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js @@ -1,4 +1,4 @@ -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoader } from '@gitlab/ui'; import { nextTick } from 'vue'; import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -61,7 +61,7 @@ describe('ValueStreamMetrics', () => { it('will display a loader with pending requests', async () => { await nextTick(); - expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); describe('with data loaded', () => { @@ -88,7 +88,7 @@ describe('ValueStreamMetrics', () => { }); it('will not display a loading icon', () => { - expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); }); describe('filterFn', () => { diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 30d5f89d2f6..46f83ac89e5 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -53,11 +53,13 @@ const defaultProps = { showVisibilityConfirmModal: false, }; +const FEATURE_ACCESS_LEVEL_ANONYMOUS = 30; + describe('Settings Panel', () => { let wrapper; const mountComponent = ( - { currentSettings = {}, ...customProps } = {}, + { currentSettings = {}, glFeatures = {}, ...customProps } = {}, mountFn = shallowMount, ) => { const propsData = { @@ -68,6 +70,12 @@ describe('Settings Panel', () => { return mountFn(settingsPanel, { propsData, + provide: { + glFeatures: { + packageRegistryAccessLevel: false, + ...glFeatures, + }, + }, }); }; @@ -95,6 +103,10 @@ describe('Settings Panel', () => { const findContainerRegistryAccessLevelInput = () => wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]'); const findPackageSettings = () => wrapper.find({ ref: 'package-settings' }); + const findPackageAccessLevel = () => + wrapper.find('[data-testid="package-registry-access-level"]'); + const findPackageAccessLevels = () => + wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]'); const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]'); const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' }); const findPagesAccessLevels = () => @@ -521,6 +533,101 @@ describe('Settings Panel', () => { settingsPanel.i18n.packagesLabel, ); }); + + it('should hide the package access level settings', () => { + wrapper = mountComponent(); + + expect(findPackageAccessLevel().exists()).toBe(false); + }); + + describe('packageRegistryAccessLevel feature flag = true', () => { + it('should hide the packages settings', () => { + wrapper = mountComponent({ + glFeatures: { packageRegistryAccessLevel: true }, + packagesAvailable: true, + }); + + expect(findPackageSettings().exists()).toBe(false); + }); + + it('should hide the package access level settings with packagesAvailable = false', () => { + wrapper = mountComponent({ glFeatures: { packageRegistryAccessLevel: true } }); + + expect(findPackageAccessLevel().exists()).toBe(false); + }); + + it('renders the package access level settings with packagesAvailable = true', () => { + wrapper = mountComponent({ + glFeatures: { packageRegistryAccessLevel: true }, + packagesAvailable: true, + }); + + expect(findPackageAccessLevel().exists()).toBe(true); + }); + + it.each` + visibilityLevel | output + ${visibilityOptions.PRIVATE} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]} + ${visibilityOptions.INTERNAL} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]} + ${visibilityOptions.PUBLIC} | ${[[30, 'Everyone']]} + `( + 'renders correct options when visibilityLevel is $visibilityLevel', + async ({ visibilityLevel, output }) => { + wrapper = mountComponent({ + glFeatures: { packageRegistryAccessLevel: true }, + packagesAvailable: true, + currentSettings: { + visibilityLevel, + }, + }); + + expect(findPackageAccessLevels().props('options')).toStrictEqual(output); + }, + ); + + it.each` + initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption + ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE} + ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS} + ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE} + `( + 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel', + async ({ + initialProjectVisibilityLevel, + newProjectVisibilityLevel, + initialPackageRegistryOption, + expectedPackageRegistryOption, + }) => { + wrapper = mountComponent({ + glFeatures: { packageRegistryAccessLevel: true }, + packagesAvailable: true, + currentSettings: { + visibilityLevel: initialProjectVisibilityLevel, + packageRegistryAccessLevel: initialPackageRegistryOption, + }, + }); + + await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel); + + expect(findPackageAccessLevels().props('value')).toStrictEqual( + expectedPackageRegistryOption, + ); + }, + ); + }); }); describe('Pages', () => { diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js index e435c0dcc08..bf5d15516c2 100644 --- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js +++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js @@ -1,9 +1,12 @@ import { getByRole } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; +import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants'; describe('First pipeline card', () => { let wrapper; + let trackingSpy; const defaultProvide = { runnerHelpPagePath: '/help/runners', @@ -17,7 +20,7 @@ describe('First pipeline card', () => { }); }; - const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }); const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i); const findInstructionsList = () => wrapper.find('ol'); const findAllInstructions = () => findInstructionsList().findAll('li'); @@ -40,6 +43,26 @@ describe('First pipeline card', () => { }); it('renders the link', () => { - expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath); + expect(findRunnersLink().href).toContain(defaultProvide.runnerHelpPagePath); + }); + + describe('tracking', () => { + beforeEach(() => { + createComponent(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks runners help page click', async () => { + const { label } = pipelineEditorTrackingOptions; + const { runners } = pipelineEditorTrackingOptions.actions.helpDrawerLinks; + + await findRunnersLink().click(); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, runners, { label }); + }); }); }); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js index 3c8821d05a7..49177befe0e 100644 --- a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js +++ b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js @@ -1,9 +1,12 @@ import { getByRole } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue'; +import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants'; describe('Pipeline config reference card', () => { let wrapper; + let trackingSpy; const defaultProvide = { ciExamplesHelpPagePath: 'help/ci/examples/', @@ -20,7 +23,7 @@ describe('Pipeline config reference card', () => { }); }; - const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }); const findCiExamplesLink = () => getLinkByName(/CI\/CD examples and templates/i); const findCiIntroLink = () => getLinkByName(/GitLab CI\/CD concepts/i); const findNeedsLink = () => getLinkByName(/Needs keyword/i); @@ -43,9 +46,44 @@ describe('Pipeline config reference card', () => { }); it('renders the links', () => { - expect(findCiExamplesLink()).toContain(defaultProvide.ciExamplesHelpPagePath); - expect(findCiIntroLink()).toContain(defaultProvide.ciHelpPagePath); - expect(findNeedsLink()).toContain(defaultProvide.needsHelpPagePath); - expect(findYmlSyntaxLink()).toContain(defaultProvide.ymlHelpPagePath); + expect(findCiExamplesLink().href).toContain(defaultProvide.ciExamplesHelpPagePath); + expect(findCiIntroLink().href).toContain(defaultProvide.ciHelpPagePath); + expect(findNeedsLink().href).toContain(defaultProvide.needsHelpPagePath); + expect(findYmlSyntaxLink().href).toContain(defaultProvide.ymlHelpPagePath); + }); + + describe('tracking', () => { + beforeEach(() => { + createComponent(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + const testTracker = async (element, expectedAction) => { + const { label } = pipelineEditorTrackingOptions; + + await element.click(); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, expectedAction, { + label, + }); + }; + + it('tracks help page links', async () => { + const { + CI_EXAMPLES_LINK, + CI_HELP_LINK, + CI_NEEDS_LINK, + CI_YAML_LINK, + } = pipelineEditorTrackingOptions.actions.helpDrawerLinks; + + testTracker(findCiExamplesLink(), CI_EXAMPLES_LINK); + testTracker(findCiIntroLink(), CI_HELP_LINK); + testTracker(findNeedsLink(), CI_NEEDS_LINK); + testTracker(findYmlSyntaxLink(), CI_YAML_LINK); + }); }); }); diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js index 8f50325295e..930f08ef545 100644 --- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js +++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js @@ -29,6 +29,17 @@ describe('CI Editor Header', () => { unmockTracking(); }); + const testTracker = async (element, expectedAction) => { + const { label } = pipelineEditorTrackingOptions; + + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await element.vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, expectedAction, { + label, + }); + }; + describe('link button', () => { beforeEach(() => { createComponent(); @@ -48,13 +59,9 @@ describe('CI Editor Header', () => { }); it('tracks the click on the browse button', async () => { - const { label, actions } = pipelineEditorTrackingOptions; - - await findLinkBtn().vm.$emit('click'); + const { browseTemplates } = pipelineEditorTrackingOptions.actions; - expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.browse_templates, { - label, - }); + testTracker(findLinkBtn(), browseTemplates); }); }); @@ -72,21 +79,31 @@ describe('CI Editor Header', () => { }); describe('when pipeline editor drawer is closed', () => { - it('emits open drawer event when clicked', () => { + beforeEach(() => { createComponent({ showDrawer: false }); + }); + it('emits open drawer event when clicked', () => { expect(wrapper.emitted('open-drawer')).toBeUndefined(); findHelpBtn().vm.$emit('click'); expect(wrapper.emitted('open-drawer')).toHaveLength(1); }); + + it('tracks open help drawer action', async () => { + const { actions } = pipelineEditorTrackingOptions; + + testTracker(findHelpBtn(), actions.openHelpDrawer); + }); }); describe('when pipeline editor drawer is open', () => { - it('emits close drawer event when clicked', () => { + beforeEach(() => { createComponent({ showDrawer: true }); + }); + it('emits close drawer event when clicked', () => { expect(wrapper.emitted('close-drawer')).toBeUndefined(); findHelpBtn().vm.$emit('click'); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 9b1cfeac4c5..fb3c04431ad 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -274,6 +274,7 @@ ci_pipelines: - security_findings - daily_build_group_report_results - latest_builds +- latest_successful_builds - daily_report_results - latest_builds_report_results - messages diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index e06fcb0cd3f..08e4c8610c9 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -585,6 +585,7 @@ ProjectFeature: - operations_access_level - security_and_compliance_access_level - container_registry_access_level +- package_registry_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 77359eb36c8..227750b6522 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -303,7 +303,7 @@ RSpec.describe Ci::JobArtifact do end describe '.created_at_before' do - it 'returns artifacts created before' do + it 'returns artifacts' do artifact1 = create(:ci_job_artifact, created_at: 1.day.ago) _artifact2 = create(:ci_job_artifact, created_at: 1.day.from_now) @@ -311,8 +311,17 @@ RSpec.describe Ci::JobArtifact do end end + describe '.id_before' do + it 'returns artifacts' do + artifact1 = create(:ci_job_artifact) + artifact2 = create(:ci_job_artifact) + + expect(described_class.id_before(artifact2.id)).to match_array([artifact1, artifact2]) + end + end + describe '.id_after' do - it 'returns artifacts id after' do + it 'returns artifacts' do artifact1 = create(:ci_job_artifact) artifact2 = create(:ci_job_artifact) @@ -320,12 +329,12 @@ RSpec.describe Ci::JobArtifact do end end - describe '.ordered_by_created_at_and_id_asc' do - it 'returns artifacts by ascending created_at ids' do + describe '.ordered_by_id' do + it 'returns artifacts in asc order' do artifact1 = create(:ci_job_artifact) artifact2 = create(:ci_job_artifact) - expect(described_class.ordered_by_created_at_and_id_asc).to eq([artifact1, artifact2]) + expect(described_class.ordered_by_id).to eq([artifact1, artifact2]) end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 9ab17ea6af7..37399f29b27 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -73,6 +73,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#latest_successful_builds' do + it 'has a one to many relationship with its latest successful builds' do + _old_build = create(:ci_build, :retried, pipeline: pipeline) + _expired_build = create(:ci_build, :expired, pipeline: pipeline) + _failed_builds = create_list(:ci_build, 2, :failed, pipeline: pipeline) + successful_builds = create_list(:ci_build, 2, :success, pipeline: pipeline) + + expect(pipeline.latest_successful_builds).to contain_exactly(successful_builds.first, successful_builds.second) + end + end + describe '#downloadable_artifacts' do let_it_be(:build) { create(:ci_build, pipeline: pipeline) } let_it_be(:downloadable_artifact) { create(:ci_job_artifact, :codequality, job: build) } diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 62c9a041a85..f2dc8464e86 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe ProjectFeaturesCompatibility do let(:project) { create(:project) } let(:features_enabled) { %w(issues wiki builds merge_requests snippets security_and_compliance) } - let(:features) { features_enabled + %w(repository pages operations container_registry) } + let(:features) { features_enabled + %w(repository pages operations container_registry package_registry) } # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table # All those fields got moved to a new table called project_feature and are now integers instead of booleans diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index ce6b59a5f95..87a2f01badc 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -640,6 +640,143 @@ RSpec.describe Deployment do is_expected.to contain_exactly(deployment1, deployment2) end end + + describe 'last_deployment_group_for_environment' do + def subject_method(environment) + described_class.last_deployment_group_for_environment(environment) + end + + let!(:project) { create(:project, :repository) } + let!(:environment) { create(:environment, project: project) } + + context 'when there are no deployments and builds' do + it do + expect(subject_method(environment)).to eq(Deployment.none) + end + end + + context 'when there are no successful builds' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:ci_build) { create(:ci_build, :running, project: project, pipeline: pipeline) } + + before do + create(:deployment, :success, environment: environment, project: project, deployable: ci_build) + end + + it do + expect(subject_method(environment)).to eq(Deployment.none) + end + end + + context 'when there are deployments for multiple pipelines' do + let(:pipeline_a) { create(:ci_pipeline, project: project) } + let(:pipeline_b) { create(:ci_pipeline, project: project) } + let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } + let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } + + # Successful deployments for pipeline_a + let!(:deployment_a) do + create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) + end + + let!(:deployment_b) do + create(:deployment, :success, project: project, environment: environment, deployable: ci_build_c) + end + + before do + # Failed deployment for pipeline_a + create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d) + + # Failed deployment for pipeline_b + create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) + end + + it 'returns the successful deployment jobs for the last deployment pipeline' do + expect(subject_method(environment).pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) + end + end + + context 'when there are many environments' do + let(:environment_b) { create(:environment, project: project) } + + let(:pipeline_a) { create(:ci_pipeline, project: project) } + let(:pipeline_b) { create(:ci_pipeline, project: project) } + let(:pipeline_c) { create(:ci_pipeline, project: project) } + let(:pipeline_d) { create(:ci_pipeline, project: project) } + + # Builds for first environment: 'environment' with pipeline_a and pipeline_b + let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } + let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } + let!(:stop_env_a) { create(:ci_build, :manual, project: project, pipeline: pipeline_a, name: 'stop_env_a') } + + # Builds for second environment: 'environment_b' with pipeline_c and pipeline_d + let(:ci_build_e) { create(:ci_build, :success, project: project, pipeline: pipeline_c) } + let(:ci_build_f) { create(:ci_build, :failed, project: project, pipeline: pipeline_d) } + let(:ci_build_g) { create(:ci_build, :success, project: project, pipeline: pipeline_c) } + let(:ci_build_h) { create(:ci_build, :failed, project: project, pipeline: pipeline_c) } + let!(:stop_env_b) { create(:ci_build, :manual, project: project, pipeline: pipeline_c, name: 'stop_env_b') } + + # Successful deployments for 'environment' from pipeline_a + let!(:deployment_a) do + create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) + end + + let!(:deployment_b) do + create(:deployment, :success, + project: project, environment: environment, deployable: ci_build_c, on_stop: 'stop_env_a') + end + + # Successful deployments for 'environment_b' from pipeline_c + let!(:deployment_c) do + create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_e) + end + + let!(:deployment_d) do + create(:deployment, :success, + project: project, environment: environment_b, deployable: ci_build_g, on_stop: 'stop_env_b') + end + + before do + # Failed deployment for 'environment' from pipeline_a and pipeline_b + create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d) + create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) + + # Failed deployment for 'environment_b' from pipeline_c and pipeline_d + create(:deployment, :failed, project: project, environment: environment_b, deployable: ci_build_h) + create(:deployment, :failed, project: project, environment: environment_b, deployable: ci_build_f) + end + + it 'batch loads for environments' do + environments = [environment, environment_b] + + # Loads Batch loader + environments.each do |env| + subject_method(env) + end + + expect(subject_method(environments.first).pluck(:id)) + .to contain_exactly(deployment_a.id, deployment_b.id) + + expect { subject_method(environments.second).pluck(:id) }.not_to exceed_query_limit(0) + + expect(subject_method(environments.second).pluck(:id)) + .to contain_exactly(deployment_c.id, deployment_d.id) + + expect(subject_method(environments.first).map(&:stop_action).compact) + .to contain_exactly(stop_env_a) + + expect { subject_method(environments.second).map(&:stop_action) } + .not_to exceed_query_limit(0) + + expect(subject_method(environments.second).map(&:stop_action).compact) + .to contain_exactly(stop_env_b) + end + end + end end describe 'latest_for_sha' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 5a1697d2bb8..5fe3882cee9 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -479,7 +479,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end context 'when matching action is defined' do - let(:build) { create(:ci_build) } + let(:build) { create(:ci_build, :success) } let!(:deployment) do create(:deployment, :success, @@ -549,7 +549,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do context 'when matching action is defined' do let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build_a) { create(:ci_build, pipeline: pipeline) } + let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } before do create(:deployment, :success, @@ -650,8 +650,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do context 'when there are more then one stop action for the environment' do let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build_a) { create(:ci_build, pipeline: pipeline) } - let(:build_b) { create(:ci_build, pipeline: pipeline) } + let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } + let(:build_b) { create(:ci_build, :success, pipeline: pipeline) } let!(:close_actions) do [ @@ -684,9 +684,9 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do expect(actions.pluck(:user)).to match_array(close_actions.pluck(:user)) end - context 'when there are failed deployment jobs' do + context 'when there are failed builds' do before do - create(:ci_build, pipeline: pipeline, name: 'close_app_c') + create(:ci_build, :failed, pipeline: pipeline, name: 'close_app_c') create(:deployment, :failed, environment: environment, @@ -694,11 +694,11 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do on_stop: 'close_app_c') end - it 'returns only stop actions from successful deployment jobs' do + it 'returns only stop actions from successful builds' do actions = subject expect(actions).to match_array(close_actions) - expect(actions.count).to eq(environment.successful_deployments.count) + expect(actions.count).to eq(pipeline.latest_successful_builds.count) end end end @@ -715,8 +715,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do context 'when there are multiple deployments with actions' do let(:pipeline) { create(:ci_pipeline, project: project) } - let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline) } - let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline) } + let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline) } + let(:ci_build_b) { create(:ci_build, :success, project: project, pipeline: pipeline) } let!(:ci_build_c) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_a') } let!(:ci_build_d) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_b') } @@ -732,7 +732,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do before do # Create failed deployment without stop_action. - build = create(:ci_build, project: project, pipeline: pipeline) + build = create(:ci_build, :failed, project: project, pipeline: pipeline) create(:deployment, :failed, project: project, environment: environment, deployable: build) end @@ -754,10 +754,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do context 'when there are deployments for multiple pipelines' do let(:pipeline_a) { create(:ci_pipeline, project: project) } let(:pipeline_b) { create(:ci_pipeline, project: project) } - let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline_a) } - let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline_b) } - let(:ci_build_c) { create(:ci_build, project: project, pipeline: pipeline_a) } - let(:ci_build_d) { create(:ci_build, project: project, pipeline: pipeline_a) } + let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } + let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } + let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } # Successful deployments for pipeline_a let!(:deployment_a) { create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) } @@ -774,6 +774,16 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do it 'returns the successful deployment jobs for the last deployment pipeline' do expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(batch_load_environment_last_deployment_group: false) + end + + it 'returns the successful deployment jobs for the last deployment pipeline' do + expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) + end + end end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 941f6c0a49d..dae0f84eda3 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -41,7 +41,7 @@ RSpec.describe ProjectFeature do end end - it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i(pages) do + it_behaves_like 'access level validation', ProjectFeature::FEATURES - %i(pages package_registry) do let(:container_features) { project.project_feature } end @@ -170,6 +170,10 @@ RSpec.describe ProjectFeature do expect(described_class.required_minimum_access_level(:repository)).to eq(Gitlab::Access::GUEST) end + it 'handles package registry' do + expect(described_class.required_minimum_access_level(:package_registry)).to eq(Gitlab::Access::REPORTER) + end + it 'raises error if feature is invalid' do expect do described_class.required_minimum_access_level(:foos) @@ -243,6 +247,50 @@ RSpec.describe ProjectFeature do end end + describe 'package_registry_access_level' do + context 'with default value' do + where(:config_packages_enabled, :expected_result) do + false | ProjectFeature::DISABLED + true | ProjectFeature::ENABLED + nil | ProjectFeature::DISABLED + end + + with_them do + it 'creates project_feature with correct package_registry_access_level' do + stub_packages_setting(enabled: config_packages_enabled) + project = Project.new + + expect(project.project_feature.package_registry_access_level).to eq(expected_result) + end + end + end + + context 'sync packages_enabled' do + # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + where(:initial_value, :new_value, :expected_result) do + ProjectFeature::DISABLED | ProjectFeature::DISABLED | false + ProjectFeature::DISABLED | ProjectFeature::ENABLED | true + ProjectFeature::DISABLED | ProjectFeature::PUBLIC | true + ProjectFeature::ENABLED | ProjectFeature::DISABLED | false + ProjectFeature::ENABLED | ProjectFeature::ENABLED | true + ProjectFeature::ENABLED | ProjectFeature::PUBLIC | true + ProjectFeature::PUBLIC | ProjectFeature::DISABLED | false + ProjectFeature::PUBLIC | ProjectFeature::ENABLED | true + ProjectFeature::PUBLIC | ProjectFeature::PUBLIC | true + end + # rubocop:enable Lint/BinaryOperatorWithIdenticalOperands + + with_them do + it 'set correct value' do + project = create(:project, package_registry_access_level: initial_value) + + project.project_feature.update!(package_registry_access_level: new_value) + + expect(project.packages_enabled).to eq(expected_result) + end + end + end + end # rubocop:disable Gitlab/FeatureAvailableUsage describe '#feature_available?' do let(:features) { ProjectFeature::FEATURES } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b1174d16adb..44d4b99bc36 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7348,6 +7348,44 @@ RSpec.describe Project, factory_default: :keep do subject { create(:project).packages_enabled } it { is_expected.to be true } + + context 'when packages_enabled is enabled' do + where(:project_visibility, :expected_result) do + Gitlab::VisibilityLevel::PRIVATE | ProjectFeature::PRIVATE + Gitlab::VisibilityLevel::INTERNAL | ProjectFeature::ENABLED + Gitlab::VisibilityLevel::PUBLIC | ProjectFeature::PUBLIC + end + + with_them do + it 'set package_registry_access_level to correct value' do + project = create(:project, + visibility_level: project_visibility, + packages_enabled: false, + package_registry_access_level: ProjectFeature::DISABLED + ) + + project.update!(packages_enabled: true) + + expect(project.package_registry_access_level).to eq(expected_result) + end + end + end + + context 'when packages_enabled is disabled' do + Gitlab::VisibilityLevel.options.values.each do |project_visibility| + it 'set package_registry_access_level to DISABLED' do + project = create(:project, + visibility_level: project_visibility, + packages_enabled: true, + package_registry_access_level: ProjectFeature::PUBLIC + ) + + project.update!(packages_enabled: false) + + expect(project.package_registry_access_level).to eq(ProjectFeature::DISABLED) + end + end + end end describe '#related_group_ids' do diff --git a/spec/models/projects/build_artifacts_size_refresh_spec.rb b/spec/models/projects/build_artifacts_size_refresh_spec.rb index cb1455b430f..f8cd8fb5c76 100644 --- a/spec/models/projects/build_artifacts_size_refresh_spec.rb +++ b/spec/models/projects/build_artifacts_size_refresh_spec.rb @@ -49,7 +49,7 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do describe '#process!' do context 'when refresh state is created' do - let!(:refresh) do + let_it_be_with_reload(:refresh) do create( :project_build_artifacts_size_refresh, :created, @@ -59,25 +59,31 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do ) end + let!(:last_job_artifact_id_on_refresh_start) { create(:ci_job_artifact, project: refresh.project) } + before do stats = create(:project_statistics, project: refresh.project, build_artifacts_size: 120) stats.increment_counter(:build_artifacts_size, 30) end it 'transitions the state to running' do - expect { refresh.process! }.to change { refresh.reload.state }.to(described_class::STATES[:running]) + expect { refresh.process! }.to change { refresh.state }.to(described_class::STATES[:running]) end it 'sets the refresh_started_at' do - expect { refresh.process! }.to change { refresh.reload.refresh_started_at.to_i }.to(now.to_i) + expect { refresh.process! }.to change { refresh.refresh_started_at.to_i }.to(now.to_i) + end + + it 'sets last_job_artifact_id_on_refresh_start' do + expect { refresh.process! }.to change { refresh.last_job_artifact_id_on_refresh_start.to_i }.to(last_job_artifact_id_on_refresh_start.id) end it 'bumps the updated_at' do - expect { refresh.process! }.to change { refresh.reload.updated_at.to_i }.to(now.to_i) + expect { refresh.process! }.to change { refresh.updated_at.to_i }.to(now.to_i) end it 'resets the build artifacts size stats' do - expect { refresh.process! }.to change { refresh.project.statistics.reload.build_artifacts_size }.to(0) + expect { refresh.process! }.to change { refresh.project.statistics.build_artifacts_size }.to(0) end it 'resets the counter attribute to zero' do @@ -214,7 +220,8 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do project: project, updated_at: 2.days.ago, refresh_started_at: 10.days.ago, - last_job_artifact_id: artifact_1.id + last_job_artifact_id: artifact_1.id, + last_job_artifact_id_on_refresh_start: artifact_3.id ) end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index eb6f81c2810..35844631287 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -122,6 +122,7 @@ project_feature: - id - created_at - metrics_dashboard_access_level + - package_registry_access_level - project_id - updated_at computed_attributes: diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 47d2c8b3794..05644dad151 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -212,7 +212,10 @@ RSpec.describe EnvironmentSerializer do upcoming_deployment = nil create(:environment, project: project).tap do |environment| create(:deployment, :success, environment: environment, project: project) - last_deployment = create(:deployment, :success, environment: environment, project: project) + + create(:ci_build, :success, project: project).tap do |build| + last_deployment = create(:deployment, :success, environment: environment, project: project, deployable: build) + end create(:deployment, :running, environment: environment, project: project) upcoming_deployment = create(:deployment, :running, environment: environment, project: project) @@ -227,11 +230,22 @@ RSpec.describe EnvironmentSerializer do def create_environment_with_associations(project) create(:environment, project: project).tap do |environment| - pipeline = create(:ci_pipeline, project: project) - manual_build = create(:ci_build, :manual, project: project, pipeline: pipeline, environment: environment.name) - scheduled_build = create(:ci_build, :scheduled, project: project, pipeline: pipeline, environment: environment.name) - create(:deployment, :success, environment: environment, project: project, deployable: manual_build) - create(:deployment, :running, environment: environment, project: project, deployable: scheduled_build) + create(:ci_pipeline, project: project).tap do |pipeline| + create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'stop-action', + environment: environment.name) + + create(:ci_build, :scheduled, project: project, pipeline: pipeline, + environment: environment.name).tap do |scheduled_build| + create(:deployment, :running, environment: environment, project: project, + deployable: scheduled_build) + end + + create(:ci_build, :success, :manual, project: project, pipeline: pipeline, + environment: environment.name).tap do |manual_build| + create(:deployment, :success, environment: environment, project: project, + deployable: manual_build, on_stop: 'stop-action') + end + end end end end diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb index 727898274cb..3ed8a0b1da0 100644 --- a/spec/services/environments/stop_service_spec.rb +++ b/spec/services/environments/stop_service_spec.rb @@ -78,11 +78,6 @@ RSpec.describe Environments::StopService do describe '#execute_for_branch' do context 'when environment with review app exists' do - before do - create(:environment, :with_review_app, project: project, - ref: 'feature') - end - context 'when user has permission to stop environment' do before do project.add_developer(user) @@ -90,25 +85,25 @@ RSpec.describe Environments::StopService do context 'when environment is associated with removed branch' do it 'stops environment' do - expect_environment_stopping_on('feature') + expect_environment_stopping_on('feature', feature_environment) end end context 'when environment is associated with different branch' do it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end context 'when specified branch does not exist' do it 'does not stop environment' do - expect_environment_not_stopped_on('non/existent/branch') + expect_environment_not_stopped_on('non/existent/branch', feature_environment) end end context 'when no branch not specified' do it 'does not stop environment' do - expect_environment_not_stopped_on(nil) + expect_environment_not_stopped_on(nil, feature_environment) end end @@ -120,7 +115,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('feature') + expect_environment_not_stopped_on('feature', feature_environment) end end end @@ -132,7 +127,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -145,7 +140,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -161,7 +156,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -190,7 +185,7 @@ RSpec.describe Environments::StopService do merge_requests_as_head_pipeline: [merge_request]) end - let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) } + let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, :success, pipeline: pipeline, project: project) } let!(:stop_review_job) { create(:ci_build, :with_deployment, :stop_review_app, :manual, pipeline: pipeline, project: project) } before do @@ -275,18 +270,22 @@ RSpec.describe Environments::StopService do end end - def expect_environment_stopped_on(branch) + def expect_environment_stopped_on(branch, environment) expect { service.execute_for_branch(branch) } - .to change { Environment.last.state }.from('available').to('stopped') + .to change { environment.reload.state }.from('available').to('stopped') end - def expect_environment_stopping_on(branch) + def expect_environment_stopping_on(branch, environment) expect { service.execute_for_branch(branch) } - .to change { Environment.last.state }.from('available').to('stopping') + .to change { environment.reload.state }.from('available').to('stopping') end - def expect_environment_not_stopped_on(branch) + def expect_environment_not_stopped_on(branch, environment) expect { service.execute_for_branch(branch) } - .not_to change { Environment.last.state } + .not_to change { environment.reload.state }.from('available') + end + + def feature_environment + create(:environment, :with_review_app, project: project, ref: 'feature') end end diff --git a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb index 41487e9ea48..6a715312097 100644 --- a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb +++ b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb @@ -52,6 +52,12 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl expect { service.execute }.to change { refresh.reload.last_job_artifact_id.to_i }.to(artifact_3.id) end + it 'updates the last_job_artifact_id to the ID of the last artifact from the project' do + expect { service.execute } + .to change { refresh.reload.last_job_artifact_id_on_refresh_start.to_i } + .to(project.job_artifacts.last.id) + end + it 'requeues the refresh job' do service.execute expect(refresh.reload).to be_pending @@ -63,7 +69,8 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl :project_build_artifacts_size_refresh, :pending, project: project, - last_job_artifact_id: artifact_3.id + last_job_artifact_id: artifact_3.id, + last_job_artifact_id_on_refresh_start: artifact_4.id ) end @@ -77,6 +84,10 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl expect(refresh.reload.last_job_artifact_id).to eq(artifact_3.id) end + it 'keeps the last_job_artifact_id_on_refresh_start unchanged' do + expect(refresh.reload.last_job_artifact_id_on_refresh_start).to eq(artifact_4.id) + end + it 'keeps the state of the refresh record at running' do expect(refresh.reload).to be_running end diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 5a88929334b..127948549b0 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -268,10 +268,36 @@ RSpec.describe ResourceAccessTokens::CreateService do end it_behaves_like 'allows creation of bot with valid params' + + context 'when user specifies an access level of OWNER for the bot' do + let_it_be(:params) { { access_level: Gitlab::Access::OWNER } } + + context 'when the executor is a MAINTAINER' do + it 'does not add the bot user with the specified access level in the resource' do + response = subject + + expect(response.error?).to be true + expect(response.errors).to include('Could not provision owner access to project access token') + end + end + + context 'when the executor is an OWNER' do + let_it_be(:user) { project.first_owner } + + it 'adds the bot user with the specified access level in the resource' do + response = subject + + access_token = response.payload[:access_token] + bot_user = access_token.user + + expect(resource.members.owners.map(&:user_id)).to include(bot_user.id) + end + end + end end end - context 'when resource is a project' do + context 'when resource is a group' do let_it_be(:resource_type) { 'group' } let_it_be(:resource) { group } @@ -283,6 +309,18 @@ RSpec.describe ResourceAccessTokens::CreateService do end it_behaves_like 'allows creation of bot with valid params' + + context 'when user specifies an access level of OWNER for the bot' do + let_it_be(:params) { { access_level: Gitlab::Access::OWNER } } + + it 'adds the bot user with the specified access level in the resource' do + response = subject + access_token = response.payload[:access_token] + bot_user = access_token.user + + expect(resource.members.owners.map(&:user_id)).to include(bot_user.id) + end + end end end end diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index e1baa594f3c..6d59943d91c 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -8,9 +8,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do create_environment_with_associations(project) create_environment_with_associations(project) - # Fix N+1 queries introduced by multi stop_actions for environment. - # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780 - relax_count = 14 + # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317 + relax_count = 1 expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count) end @@ -23,9 +22,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do create_environment_with_associations(project) create_environment_with_associations(project) - # Fix N+1 queries introduced by multi stop_actions for environment. - # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780 - relax_count = 14 + # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317 + relax_count = 1 expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count) end |