From 0fea036ba7c8d26ffeecc7f52bf770ea3a88689e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 13 Dec 2023 21:09:54 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITALY_SERVER_VERSION | 2 +- .../boards/components/board_card_inner.vue | 4 +- .../components/shared/findings_drawer.vue | 99 +++++++--- app/assets/javascripts/editor/schema/ci.json | 18 ++ .../folder/environments_folder_app.vue | 96 +++++++++- .../folder/environments_folder_bundle.js | 1 + .../graphql/queries/folder.query.graphql | 4 +- .../environments/graphql/resolvers/base.js | 17 +- .../stylesheets/page_bundles/boards.scss | 13 +- app/models/ci/build.rb | 4 - app/models/ci/job_artifact.rb | 2 - app/models/ci/pipeline.rb | 6 +- app/models/ci/pipeline_metadata.rb | 5 + app/models/concerns/ci/metadatable.rb | 6 +- .../ci/downloadable_artifact_entity.rb | 6 +- .../merge_requests/pipeline_entity.rb | 6 +- .../ci/job_artifacts/create_service.rb | 2 - app/views/layouts/project.html.haml | 1 + app/workers/click_house/events_sync_worker.rb | 2 +- ...> auto_cancel_pipeline_on_job_failure.yml} | 10 +- .../development/non_public_artifacts.yml | 8 - .../20231207173100_create_ci_used_minutes.rb | 26 +++ ...0231207173101_create_ci_used_minutes_mv.rb | 27 +++ .../backfill_vs_code_settings_uuid.yml | 9 + ..._on_job_failure_to_ci_pipeline_metadata.rb | 10 ++ ...01_queue_backfill_vs_code_settings_uuid.rb | 27 +++ db/schema_migrations/20231117211214 | 1 + db/schema_migrations/20231130140901 | 1 + db/structure.sql | 1 + doc/api/graphql/reference/index.md | 2 + doc/api/group_iterations.md | 18 +- doc/api/iterations.md | 18 +- doc/ci/resource_groups/index.md | 46 +++-- doc/ci/yaml/index.md | 1 + .../advanced_search_migration_styleguide.md | 24 +-- doc/user/application_security/sast/index.md | 2 +- doc/user/permissions.md | 1 + .../project/img/global_time_report_v16_5.png | Bin 0 -> 19421 bytes doc/user/project/time_tracking.md | 44 +++-- .../lib/click_house/client.rb | 4 + gems/csv_builder/lib/csv_builder/builder.rb | 4 +- .../backfill_vs_code_settings_uuid.rb | 21 +++ lib/gitlab/ci/config/entry/auto_cancel.rb | 7 +- .../ci/pipeline/chain/populate_metadata.rb | 20 ++- locale/gitlab.pot | 3 + qa/qa/support/api.rb | 2 +- qa/qa/tools/reliable_report.rb | 44 +++-- qa/spec/spec_helper.rb | 6 + qa/spec/tools/reliable_report_spec.rb | 66 ++++++- .../environments/environments_folder_spec.rb | 24 +++ .../findings_drawer_spec.js.snap | 14 +- .../components/shared/findings_drawer_spec.js | 115 +++++++++--- .../diffs/mock_data/findings_drawer.js | 42 +++++ .../editor/schema/ci/ci_schema_spec.js | 6 + .../negative_tests/auto_cancel_pipeline.yml | 4 + .../on_job_failure/all.yml | 4 + .../on_job_failure/none.yml | 4 + .../folder/environments_folder_app_spec.js | 10 +- .../graphql/resolvers/base_spec.js | 3 +- .../backfill_vs_code_settings_uuid_spec.rb | 74 ++++++++ .../ci/config/entry/auto_cancel_spec.rb | 27 +++ .../gitlab/ci/config/entry/workflow_spec.rb | 3 +- .../pipeline/chain/populate_metadata_spec.rb | 95 ++++++++-- spec/lib/gitlab/ci/yaml_processor_spec.rb | 6 +- ...eue_backfill_vs_code_settings_uuid_spec.rb | 26 +++ spec/models/ci/build_spec.rb | 44 +---- spec/models/ci/job_artifact_spec.rb | 10 -- spec/models/ci/pipeline_metadata_spec.rb | 8 + spec/models/ci/pipeline_spec.rb | 1 + spec/requests/api/ci/job_artifacts_spec.rb | 38 ---- .../projects/merge_requests/content_spec.rb | 8 - spec/serializers/build_details_entity_spec.rb | 10 -- .../workflow_auto_cancel_spec.rb | 169 ++++++++++++++---- .../ci/job_artifacts/create_service_spec.rb | 46 ----- .../click_house/events_sync_worker_spec.rb | 4 +- 75 files changed, 1135 insertions(+), 407 deletions(-) rename config/feature_flags/development/{explain_vulnerability.yml => auto_cancel_pipeline_on_job_failure.yml} (59%) delete mode 100644 config/feature_flags/development/non_public_artifacts.yml create mode 100644 db/click_house/migrate/main/20231207173100_create_ci_used_minutes.rb create mode 100644 db/click_house/migrate/main/20231207173101_create_ci_used_minutes_mv.rb create mode 100644 db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml create mode 100644 db/migrate/20231117211214_add_auto_cancel_on_job_failure_to_ci_pipeline_metadata.rb create mode 100644 db/post_migrate/20231130140901_queue_backfill_vs_code_settings_uuid.rb create mode 100644 db/schema_migrations/20231117211214 create mode 100644 db/schema_migrations/20231130140901 create mode 100644 doc/user/project/img/global_time_report_v16_5.png create mode 100644 lib/gitlab/background_migration/backfill_vs_code_settings_uuid.rb create mode 100644 spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml create mode 100644 spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml create mode 100644 spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml create mode 100644 spec/lib/gitlab/background_migration/backfill_vs_code_settings_uuid_spec.rb create mode 100644 spec/migrations/20231130140901_queue_backfill_vs_code_settings_uuid_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index b297a15d8d3..d02e70a98f2 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -2893c46338b2d070bfd8559d09e6c781131fb27f +144dca53fcf99bd9064cb2f6e2631f6367f0eafe diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 06a8acc0c04..97dab2e1d34 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -407,7 +407,7 @@ export default { -
+
{{ assigneeCounterLabel }} diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue index 6a141ee28ba..854f6910fa1 100644 --- a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue +++ b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue @@ -1,5 +1,5 @@ @@ -149,7 +221,7 @@ export default {
@@ -157,14 +229,28 @@ export default {
+
+
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 44a5d1e3662..0201fb53f77 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -35,6 +35,7 @@ export default () => { component: EnvironmentsFolderApp, props: (route) => ({ scope: route.query.scope, + page: Number(route.query.page || '1'), folderName, folderPath, }), diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql index 49b61c93c8c..d183f27a8b6 100644 --- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql @@ -3,8 +3,10 @@ query getEnvironmentFolder( $scope: String $search: String $perPage: Int + $page: Int ) { - folder(environment: $environment, scope: $scope, search: $search, perPage: $perPage) @client { + folder(environment: $environment, scope: $scope, search: $search, perPage: $perPage, page: $page) + @client { activeCount environments stoppedCount diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js index 8b0ac039290..c9f9a2da13a 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/base.js +++ b/app/assets/javascripts/environments/graphql/resolvers/base.js @@ -59,15 +59,18 @@ export const baseQueries = (endpoint) => ({ }; }); }, - folder(_, { environment: { folderPath }, scope, search, perPage }) { + folder(_, { environment: { folderPath }, scope, search, perPage, page }) { // eslint-disable-next-line camelcase const per_page = perPage || 3; - return axios.get(folderPath, { params: { scope, search, per_page } }).then((res) => ({ - activeCount: res.data.active_count, - environments: res.data.environments.map(mapEnvironment), - stoppedCount: res.data.stopped_count, - __typename: 'LocalEnvironmentFolder', - })); + const pageNumber = page || 1; + return axios + .get(folderPath, { params: { scope, search, per_page, page: pageNumber } }) + .then((res) => ({ + activeCount: res.data.active_count, + environments: res.data.environments.map(mapEnvironment), + stoppedCount: res.data.stopped_count, + __typename: 'LocalEnvironmentFolder', + })); }, isLastDeployment(_, { environment }) { return environment?.lastDeployment?.isLast; diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index 22e42d0a7f7..66482ef42b5 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -131,6 +131,7 @@ height: $gl-padding-24; border-radius: $gl-padding-24; font-size: $gl-font-size-xs; + position: relative; @include media-breakpoint-down(md) { min-width: auto; @@ -140,15 +141,9 @@ } } - .user-avatar-link:not(:only-child) { - margin-left: -$gl-padding; - - &:nth-of-type(1) { - z-index: 2; - } - - &:nth-of-type(2) { - z-index: 1; + .user-avatar-link { + &:not(:last-of-type) { + @include gl-mr-n3; } } diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3aaf9501493..e56f3d2536c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -729,16 +729,12 @@ module Ci end def artifacts_public? - return true if Feature.disabled?(:non_public_artifacts, project, type: :development) - return true if job_artifacts_archive.nil? # To backward compatibility return true if no artifacts found job_artifacts_archive.public_access? end def artifact_is_public_in_config? - return true if Feature.disabled?(:non_public_artifacts, project, type: :development) - artifacts_public = options.dig(:artifacts, :public) return true if artifacts_public.nil? # Default artifacts:public to true diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 0c6fcc02344..2fd71fbe463 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -362,8 +362,6 @@ module Ci end def public_access? - return true unless Feature.enabled?(:non_public_artifacts, project, type: :development) - public_accessibility? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6331b5880e0..14fd8db5dde 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -97,11 +97,11 @@ module Ci has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' - has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' has_many :downloadable_artifacts, -> do not_expired.or(where_exists(Ci::Pipeline.artifacts_locked.where("#{Ci::Pipeline.quoted_table_name}.id = #{Ci::Build.quoted_table_name}.commit_id"))).downloadable.with_job end, through: :latest_builds, source: :job_artifacts - has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable' + has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable' has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline @@ -151,7 +151,7 @@ module Ci accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :full_path, to: :project, prefix: true - delegate :name, to: :pipeline_metadata, allow_nil: true + delegate :name, :auto_cancel_on_job_failure, to: :pipeline_metadata, allow_nil: true validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb index 5457fb899b0..37fa3e32ad8 100644 --- a/app/models/ci/pipeline_metadata.rb +++ b/app/models/ci/pipeline_metadata.rb @@ -10,6 +10,11 @@ module Ci disabled: 2 }, _prefix: true + enum auto_cancel_on_job_failure: { + none: 0, + all: 1 + }, _prefix: true + belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_metadata belongs_to :project, class_name: "Project", inverse_of: :pipeline_metadata diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index c0830d237c5..3f48df08016 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -27,10 +27,8 @@ module Ci before_validation :ensure_metadata, on: :create - scope :with_project_and_metadata, ->(project) do - if Feature.enabled?(:non_public_artifacts, project, type: :development) - joins(:metadata).includes(:metadata).preload(:project) - end + scope :with_project_and_metadata, -> do + joins(:metadata).includes(:metadata).preload(:project) end end diff --git a/app/serializers/ci/downloadable_artifact_entity.rb b/app/serializers/ci/downloadable_artifact_entity.rb index c2caa371614..5c9bb351314 100644 --- a/app/serializers/ci/downloadable_artifact_entity.rb +++ b/app/serializers/ci/downloadable_artifact_entity.rb @@ -5,12 +5,10 @@ module Ci include RequestAwareEntity expose :artifacts do |pipeline, options| - artifacts = pipeline.downloadable_artifacts + downloadable_artifacts = pipeline.downloadable_artifacts project = pipeline.project - if Feature.enabled?(:non_public_artifacts, project) - artifacts = artifacts.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } - end + artifacts = downloadable_artifacts.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } BuildArtifactEntity.represent(artifacts, options.merge(project: project)) end diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb index ef4debbd89f..81f68657e38 100644 --- a/app/serializers/merge_requests/pipeline_entity.rb +++ b/app/serializers/merge_requests/pipeline_entity.rb @@ -27,11 +27,9 @@ class MergeRequests::PipelineEntity < Grape::Entity rel = pipeline.downloadable_artifacts project = pipeline.project - if Feature.enabled?(:non_public_artifacts, project, type: :development) - rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } - end + allowed_to_read_artifacts = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } - BuildArtifactEntity.represent(rel, options.merge(project: project)) + BuildArtifactEntity.represent(allowed_to_read_artifacts, options.merge(project: project)) end expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline| diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb index 5927e9602b9..0791fff8545 100644 --- a/app/services/ci/job_artifacts/create_service.rb +++ b/app/services/ci/job_artifacts/create_service.rb @@ -128,8 +128,6 @@ module Ci def accessibility(params) accessibility = params[:accessibility] - return :public if Feature.disabled?(:non_public_artifacts, project, type: :development) - return accessibility if accessibility.present? job.artifact_is_public_in_config? ? :public : :private diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 368f9998860..e95d645769e 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -21,6 +21,7 @@ - content_for :before_content do = render 'projects/invite_members_modal', project: @project += dispensable_render_if_exists "projects/importing_alert", project: @project = dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert" = dispensable_render_if_exists "projects/free_user_cap_alert", project: @project = dispensable_render_if_exists 'shared/unlimited_members_during_trial_alert', resource: @project diff --git a/app/workers/click_house/events_sync_worker.rb b/app/workers/click_house/events_sync_worker.rb index ea9d610bd52..21c10566a67 100644 --- a/app/workers/click_house/events_sync_worker.rb +++ b/app/workers/click_house/events_sync_worker.rb @@ -99,7 +99,7 @@ module ClickHouse strong_memoize_attr :last_event_id_in_postgresql def enabled? - ClickHouse::Client.configuration.databases[:main].present? && Feature.enabled?(:event_sync_worker_for_click_house) + ClickHouse::Client.database_configured?(:main) && Feature.enabled?(:event_sync_worker_for_click_house) end def next_batch diff --git a/config/feature_flags/development/explain_vulnerability.yml b/config/feature_flags/development/auto_cancel_pipeline_on_job_failure.yml similarity index 59% rename from config/feature_flags/development/explain_vulnerability.yml rename to config/feature_flags/development/auto_cancel_pipeline_on_job_failure.yml index 0b0b6f6dfb4..cbf0c518a15 100644 --- a/config/feature_flags/development/explain_vulnerability.yml +++ b/config/feature_flags/development/auto_cancel_pipeline_on_job_failure.yml @@ -1,8 +1,8 @@ --- -name: explain_vulnerability -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117472 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407282 -milestone: '15.11' +name: auto_cancel_pipeline_on_job_failure +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137815 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433163 +milestone: '16.7' type: development -group: group::threat insights +group: group::pipeline execution default_enabled: false diff --git a/config/feature_flags/development/non_public_artifacts.yml b/config/feature_flags/development/non_public_artifacts.yml deleted file mode 100644 index 74826abd7df..00000000000 --- a/config/feature_flags/development/non_public_artifacts.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: non_public_artifacts -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49775 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294503 -milestone: '13.8' -type: development -group: group::pipeline security -default_enabled: false diff --git a/db/click_house/migrate/main/20231207173100_create_ci_used_minutes.rb b/db/click_house/migrate/main/20231207173100_create_ci_used_minutes.rb new file mode 100644 index 00000000000..901b0eed756 --- /dev/null +++ b/db/click_house/migrate/main/20231207173100_create_ci_used_minutes.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class CreateCiUsedMinutes < ClickHouse::Migration + def up + execute <<~SQL + CREATE TABLE IF NOT EXISTS ci_used_minutes + ( + project_id UInt64 DEFAULT 0, + status LowCardinality(String) DEFAULT '', + runner_type UInt8 DEFAULT 0, + finished_at_bucket DateTime64(6, 'UTC') DEFAULT now64(), + + count_builds AggregateFunction(count), + total_duration SimpleAggregateFunction(sum, Int64) + ) + ENGINE = AggregatingMergeTree() + ORDER BY (finished_at_bucket, project_id, status, runner_type) + SQL + end + + def down + execute <<~SQL + DROP TABLE ci_used_minutes + SQL + end +end diff --git a/db/click_house/migrate/main/20231207173101_create_ci_used_minutes_mv.rb b/db/click_house/migrate/main/20231207173101_create_ci_used_minutes_mv.rb new file mode 100644 index 00000000000..856710b660e --- /dev/null +++ b/db/click_house/migrate/main/20231207173101_create_ci_used_minutes_mv.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class CreateCiUsedMinutesMv < ClickHouse::Migration + def up + execute <<~SQL + CREATE MATERIALIZED VIEW IF NOT EXISTS ci_used_minutes_mv + TO ci_used_minutes + AS + SELECT + project_id, + status, + runner_type, + toStartOfInterval(finished_at, INTERVAL 1 day) AS finished_at_bucket, + + countState() AS count_builds, + sumSimpleState(duration) AS total_duration + FROM ci_finished_builds + GROUP BY project_id, status, runner_type, finished_at_bucket + SQL + end + + def down + execute <<~SQL + DROP VIEW ci_used_minutes_mv + SQL + end +end diff --git a/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml b/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml new file mode 100644 index 00000000000..022752a06a7 --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: BackfillVsCodeSettingsUuid +description: Backfills the uuid column of existing vs_code_settings records +feature_category: web_ide +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138355 +milestone: '16.7' +queued_migration_version: 20231130140901 +finalize_after: '2023-12-17' +finalized_by: # version of the migration that finalized this BBM diff --git a/db/migrate/20231117211214_add_auto_cancel_on_job_failure_to_ci_pipeline_metadata.rb b/db/migrate/20231117211214_add_auto_cancel_on_job_failure_to_ci_pipeline_metadata.rb new file mode 100644 index 00000000000..affa08ff0c3 --- /dev/null +++ b/db/migrate/20231117211214_add_auto_cancel_on_job_failure_to_ci_pipeline_metadata.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddAutoCancelOnJobFailureToCiPipelineMetadata < Gitlab::Database::Migration[2.2] + milestone '16.7' + enable_lock_retries! + + def change + add_column :ci_pipeline_metadata, :auto_cancel_on_job_failure, :smallint, default: 0, null: false + end +end diff --git a/db/post_migrate/20231130140901_queue_backfill_vs_code_settings_uuid.rb b/db/post_migrate/20231130140901_queue_backfill_vs_code_settings_uuid.rb new file mode 100644 index 00000000000..7acfed7d154 --- /dev/null +++ b/db/post_migrate/20231130140901_queue_backfill_vs_code_settings_uuid.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueBackfillVsCodeSettingsUuid < Gitlab::Database::Migration[2.2] + milestone "16.7" + + MIGRATION = "BackfillVsCodeSettingsUuid" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :vs_code_settings, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :vs_code_settings, :id, []) + end +end diff --git a/db/schema_migrations/20231117211214 b/db/schema_migrations/20231117211214 new file mode 100644 index 00000000000..4a7cea0d621 --- /dev/null +++ b/db/schema_migrations/20231117211214 @@ -0,0 +1 @@ +97e91312cdf33db897b64f45ec14dc748be91fa6741a05bd119357a3c4d17017 \ No newline at end of file diff --git a/db/schema_migrations/20231130140901 b/db/schema_migrations/20231130140901 new file mode 100644 index 00000000000..405d66191a9 --- /dev/null +++ b/db/schema_migrations/20231130140901 @@ -0,0 +1 @@ +4e9b26432f7c6cfbcd2486a3867665ba50d66ca9bd49f7d70a349f222a136277 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b7f2ee2758e..32b212e9d73 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14296,6 +14296,7 @@ CREATE TABLE ci_pipeline_metadata ( pipeline_id bigint NOT NULL, name text, auto_cancel_on_new_commit smallint DEFAULT 0 NOT NULL, + auto_cancel_on_job_failure smallint DEFAULT 0 NOT NULL, CONSTRAINT check_9d3665463c CHECK ((char_length(name) <= 255)) ); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a96025b87dd..b65b50dd66c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -19756,6 +19756,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `iid` | [`ID`](#id) | Internal ID of the Iteration to look up. | | `in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. | | `includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. | +| `includeDescendants` | [`Boolean`](#boolean) | Whether to include descendant iterations. | | `iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. | | `search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. Returns all iterations if empty. | | `sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. | @@ -24886,6 +24887,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `iid` | [`ID`](#id) | Internal ID of the Iteration to look up. | | `in` | [`[IterationSearchableField!]`](#iterationsearchablefield) | Fields in which the fuzzy-search should be performed with the query given in the argument `search`. Defaults to `[title]`. | | `includeAncestors` | [`Boolean`](#boolean) | Whether to include ancestor iterations. Defaults to true. | +| `includeDescendants` | [`Boolean`](#boolean) | Whether to include descendant iterations. | | `iterationCadenceIds` | [`[IterationsCadenceID!]`](#iterationscadenceid) | Global iteration cadence IDs by which to look up the iterations. | | `search` | [`String`](#string) | Query used for fuzzy-searching in the fields selected in the argument `in`. Returns all iterations if empty. | | `sort` | [`IterationSort`](#iterationsort) | List iterations by sort order. If unspecified, an arbitrary order (subject to change) is used. | diff --git a/doc/api/group_iterations.md b/doc/api/group_iterations.md index ef6c429e8c5..0ceef8267f5 100644 --- a/doc/api/group_iterations.md +++ b/doc/api/group_iterations.md @@ -26,18 +26,20 @@ GET /groups/:id/iterations?state=opened GET /groups/:id/iterations?state=closed GET /groups/:id/iterations?search=version GET /groups/:id/iterations?include_ancestors=false +GET /groups/:id/iterations?include_descendants=true GET /groups/:id/iterations?updated_before=2013-10-02T09%3A24%3A18Z GET /groups/:id/iterations?updated_after=2013-10-02T09%3A24%3A18Z ``` -| Attribute | Type | Required | Description | -| ------------------- | ------- | -------- | ----------- | -| `state` | string | no | 'Return `opened`, `upcoming`, `current`, `closed`, or `all` iterations.' | -| `search` | string | no | Return only iterations with a title matching the provided string. | -| `in` | array of strings | no | Fields in which fuzzy search should be performed with the query given in the argument `search`. The available options are `title` and `cadence_title`. Default is `[title]`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350991) in GitLab 16.2. | -| `include_ancestors` | boolean | no | Include iterations from parent group and its ancestors. Defaults to `true`. | -| `updated_before` | datetime | no | Return only iterations updated before the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | -| `updated_after` | datetime | no | Return only iterations updated after the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | +| Attribute | Type | Required | Description | +| --------------------- | -------- | -------- | ----------- | +| `state` | string | no | 'Return `opened`, `upcoming`, `current`, `closed`, or `all` iterations.' | +| `search` | string | no | Return only iterations with a title matching the provided string. | +| `in` | array of strings | no | Fields in which fuzzy search should be performed with the query given in the argument `search`. The available options are `title` and `cadence_title`. Default is `[title]`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350991) in GitLab 16.2. | +| `include_ancestors` | boolean | no | Include iterations for group and its ancestors. Defaults to `true`. | +| `include_descendants` | boolean | no | Include iterations for group and its descendants. Defaults to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135764) in GitLab 16.7. | +| `updated_before` | datetime | no | Return only iterations updated before the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | +| `updated_after` | datetime | no | Return only iterations updated after the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | Example request: diff --git a/doc/api/iterations.md b/doc/api/iterations.md index 9502be4a289..b89d1eeb15e 100644 --- a/doc/api/iterations.md +++ b/doc/api/iterations.md @@ -28,18 +28,20 @@ GET /projects/:id/iterations?state=opened GET /projects/:id/iterations?state=closed GET /projects/:id/iterations?search=version GET /projects/:id/iterations?include_ancestors=false +GET /projects/:id/iterations?include_descendants=true GET /projects/:id/iterations?updated_before=2013-10-02T09%3A24%3A18Z GET /projects/:id/iterations?updated_after=2013-10-02T09%3A24%3A18Z ``` -| Attribute | Type | Required | Description | -| ------------------- | ------- | -------- | ----------- | -| `state` | string | no | 'Return `opened`, `upcoming`, `current`, `closed`, or `all` iterations.' | -| `search` | string | no | Return only iterations with a title matching the provided string. | -| `in` | array of strings | no | Fields in which fuzzy search should be performed with the query given in the argument `search`. The available options are `title` and `cadence_title`. Default is `[title]`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350991) in GitLab 16.2. | -| `include_ancestors` | boolean | no | Include iterations from parent group and its ancestors. Defaults to `true`. | -| `updated_before` | datetime | no | Return only iterations updated before the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | -| `updated_after` | datetime | no | Return only iterations updated after the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | +| Attribute | Type | Required | Description | +| --------------------- | -------- | -------- | ----------- | +| `state` | string | no | 'Return `opened`, `upcoming`, `current`, `closed`, or `all` iterations.' | +| `search` | string | no | Return only iterations with a title matching the provided string. | +| `in` | array of strings | no | Fields in which fuzzy search should be performed with the query given in the argument `search`. The available options are `title` and `cadence_title`. Default is `[title]`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350991) in GitLab 16.2. | +| `include_ancestors` | boolean | no | Include iterations for parent group and its ancestors. Defaults to `true`. | +| `include_descendants` | boolean | no | Include iterations for parent group and its descendants. Defaults to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135764) in GitLab 16.7. | +| `updated_before` | datetime | no | Return only iterations updated before the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | +| `updated_after` | datetime | no | Return only iterations updated after the given datetime. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378662) in GitLab 15.10. | Example request: diff --git a/doc/ci/resource_groups/index.md b/doc/ci/resource_groups/index.md index aca272dddee..b7b9c216b14 100644 --- a/doc/ci/resource_groups/index.md +++ b/doc/ci/resource_groups/index.md @@ -259,19 +259,25 @@ Sometimes, a job hangs with the message `Waiting for resource: ` first check that the resource group is working correctly: 1. Go to the job details page. -1. Select **View job currently using resource**. -1. Check the job status: - - If the status is `running` or `pending`, the feature is working correctly. Wait until the job finishes and releases the resource. - - If the status is `created` and the [process mode](#process-modes) is either **Oldest first** or **Newest first**, the feature is working correctly. - Visit the pipeline page of the job and check which upstream stage or job is blocking the execution. - - If none of the above conditions are met, the feature might not be working correctly. - [Open a new issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new) with the following information: - - The job ID. - - The job status. - - How often the problem occurs. - - Steps to reproduce the problem. +1. If the resource is assigned to a job, select **View job currently using resource** and check the job status. -You can also get job information from the GraphQL API. You should use the GraphQL API if you use [pipeline-level concurrency control with cross-project/parent-child pipelines](#pipeline-level-concurrency-control-with-cross-projectparent-child-pipelines) because the trigger jobs are not accessible from the UI. + - If the status is `running` or `pending`, the feature is working correctly. Wait until the job finishes and releases the resource. + - If the status is `created` and the [process mode](#process-modes) is either **Oldest first** or **Newest first**, the feature is working correctly. + Visit the pipeline page of the job and check which upstream stage or job is blocking the execution. + - If none of the above conditions are met, the feature might not be working correctly. [Report the issue to GitLab](#report-an-issue). + +1. If **View job currently using resource** is not available, the resource is not assigned to a job. Instead, check the resource's upcoming jobs. + + 1. Get the resource's upcoming jobs with the [REST API](../../api/resource_groups.md#list-upcoming-jobs-for-a-specific-resource-group). + 1. Verify that the job's [process mode](#process-modes) is **Oldest first**. + 1. Find the first job in the list of upcoming jobs, and get the job details [with GraphQL](#get-job-details-through-graphql). + 1. If the first job's pipeline is an older pipeline, try to cancel the pipeline or the job itself. + 1. Optional. Repeat this process if the next upcoming job is still in an older pipeline that should no longer run. + 1. If the problem persists, [report the issue to GitLab](#report-an-issue). + +#### Get job details through GraphQL + +You can get job information from the GraphQL API. You should use the GraphQL API if you use [pipeline-level concurrency control with cross-project/parent-child pipelines](#pipeline-level-concurrency-control-with-cross-projectparent-child-pipelines) because the trigger jobs are not accessible from the UI. To get job information from the GraphQL API: @@ -284,8 +290,9 @@ To get job information from the GraphQL API: { project(fullPath: "") { name - job(id: "gid://gitlab/Ci::Bridge/") { + job(id: "gid://gitlab/Ci::Build/") { name + status detailedStatus { action { path @@ -305,7 +312,7 @@ To get job information from the GraphQL API: { project(fullPath: "") { name - job(id: "gid://gitlab/Ci::Bridge/") { + job(id: "gid://gitlab/Ci::Build/") { name status pipeline { @@ -316,4 +323,13 @@ To get job information from the GraphQL API: } ``` - If the status is not `running` or `pending`, [open a new issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new) and [contact support](https://about.gitlab.com/support/#contact-support) so they can apply the correct labels to the issue. +### Report an issue + +[Open a new issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new) with the following information: + +- The ID of the affected job. +- The job status. +- How often the problem occurs. +- Steps to reproduce the problem. + + You can also [contact support](https://about.gitlab.com/support/#contact-support) for further assistance, or to get in touch with the development team. diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 282b5437f00..0b7911ef436 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -1251,6 +1251,7 @@ job: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223273) in GitLab 13.8 [with a flag](../../user/feature_flags.md) named `non_public_artifacts`, disabled by default. > - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/322454) in GitLab 15.10. Artifacts created with `artifacts:public` before 15.10 are not guaranteed to remain private after this update. +> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/294503) in GitLab 16.7. Rolled out and removed a feature flag named `non_public_artifacts` WARNING: On self-managed GitLab, by default this feature is not available. To make it available, diff --git a/doc/development/search/advanced_search_migration_styleguide.md b/doc/development/search/advanced_search_migration_styleguide.md index 0ad3b5dc7c9..87083d1a36f 100644 --- a/doc/development/search/advanced_search_migration_styleguide.md +++ b/doc/development/search/advanced_search_migration_styleguide.md @@ -66,17 +66,15 @@ The following migration helpers are available in `ee/app/workers/concerns/elasti Backfills a specific field in an index. In most cases, the mapping for the field should already be added. -Requires the `index_name` and `field_name` methods to backfill a single field. +Requires the `field_name` method and `DOCUMENT_TYPE` constant to backfill a single field. ```ruby class MigrationName < Elastic::Migration include Elastic::MigrationBackfillHelper - private + DOCUMENT_TYPE = Issue - def index_name - Issue.__elasticsearch__.index_name - end + private def field_name :schema_version @@ -84,17 +82,15 @@ class MigrationName < Elastic::Migration end ``` -Requires the `index_name` and `field_names` methods to backfill multiple fields if any field is null. +Requires the `field_names` method and `DOCUMENT_TYPE` constant to backfill multiple fields if any field is null. ```ruby class MigrationName < Elastic::Migration include Elastic::MigrationBackfillHelper - private + DOCUMENT_TYPE = Issue - def index_name - Issue.__elasticsearch__.index_name - end + private def field_names %w[schema_version visibility_level] @@ -106,17 +102,15 @@ end Updates a mapping in an index by calling `put_mapping` with the mapping specified. -Requires the `index_name` and `new_mappings` methods. +Requires the `new_mappings` method and `DOCUMENT_TYPE` constant. ```ruby class MigrationName < Elastic::Migration include Elastic::MigrationUpdateMappingsHelper - private + DOCUMENT_TYPE = Issue - def index_name - Issue.__elasticsearch__.index_name - end + private def new_mappings { diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 77fcadd4cea..ab6d5212227 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -252,7 +252,7 @@ were introduced by the changes made in the merge request. FLAG: On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `sast_reports_in_inline_diff`. -On GitLab.com, this feature is not available. +On GitLab.com, this feature is available. SAST results display in the merge request **Changes** view. Lines containing SAST issues are marked by a symbol beside the gutter. Select the symbol to see the list of issues, then select an issue to see its details. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4796c63566f..8c15d696760 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -121,6 +121,7 @@ The following table lists project permissions available for each role: | [License Scanning](compliance/license_scanning_of_cyclonedx_files/index.md):
View License Compliance reports | ✓ (1) | ✓ | ✓ | ✓ | ✓ | | [License Scanning](compliance/license_scanning_of_cyclonedx_files/index.md):
View License list | | ✓ | ✓ | ✓ | ✓ | | [License approval policies](../user/compliance/license_approval_policies.md):
Manage license policy | | | | ✓ | ✓ | +| [Merge requests](project/merge_requests/index.md):
View a merge request | ✓ (1) | ✓ | ✓ | ✓ | ✓ | | [Merge requests](project/merge_requests/index.md):
Assign reviewer | | | ✓ | ✓ | ✓ | | [Merge requests](project/merge_requests/index.md):
View list | (25) | ✓ | ✓ | ✓ | ✓ | | [Merge requests](project/merge_requests/index.md):
Apply code change suggestions | | | ✓ | ✓ | ✓ | diff --git a/doc/user/project/img/global_time_report_v16_5.png b/doc/user/project/img/global_time_report_v16_5.png new file mode 100644 index 0000000000000000000000000000000000000000..36feb9591ae14c463bd672f1f49a0644869ea7f0 GIT binary patch literal 19421 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 [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271409) in GitLab 13.12. +### For an issue or merge request -You can view a breakdown of time spent on an issue or merge request. - -To view a time tracking report: +To view a time tracking report of time spent on an issue or merge request: 1. Go to an issue or a merge request. 1. In the right sidebar, select **Time tracking report**. @@ -167,22 +165,46 @@ To view a time tracking report: The breakdown of spent time displayed is limited to a maximum of 100 entries. +### Global time tracking report **(EXPERIMENT)** + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344002) in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `global_time_tracking_report`. Disabled by default. +> - Enabled on GitLab.com in GitLab 16.5. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `global_time_tracking_report`. +On GitLab.com, this feature is available. +This feature is not ready for production use. + +View a report of time spent in issues and merge requests across all of GitLab. + +This feature is an [Experiment](../../policy/experiment-beta-support.md). +If you find a bug, let us know in the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/435222). + +To view the global time tracking report: + +1. In your browser, enter the global report's URL: + - For self-managed, add `/-/timelogs` to your base URL. For example, `https://gitlab.example.com/-/timelogs`. + - For GitLab.com, go to . +1. Optional. To filter by a specific user, enter their username without the `@` symbol. +1. Select start and end dates. +1. Select **Run report**. + +![global time tracking report](img/global_time_report_v16_5.png) + ## Available time units The following time units are available: | Time unit | What to type | Conversion rate | | --------- | --------------------------- | --------------- | -| Month | `mo`, `month`, or `months` | 4 w (160 h) | -| Week | `w`, `week`, or `weeks` | 5 d (40 h) | -| Day | `d`, `day`, or `days` | 8 h | -| Hour | `h`, `hour`, or `hours` | 60 m | +| Month | `mo`, `month`, or `months` | 4 w (160 h) | +| Week | `w`, `week`, or `weeks` | 5 d (40 h) | +| Day | `d`, `day`, or `days` | 8 h | +| Hour | `h`, `hour`, or `hours` | 60 m | | Minute | `m`, `minute`, or `minutes` | | ### Limit displayed units to hours **(FREE SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29469/) in GitLab 12.1. - In GitLab self-managed instances, you can limit the display of time units to hours. To do so: diff --git a/gems/click_house-client/lib/click_house/client.rb b/gems/click_house-client/lib/click_house/client.rb index 1ca3653c45f..499201a1aba 100644 --- a/gems/click_house-client/lib/click_house/client.rb +++ b/gems/click_house-client/lib/click_house/client.rb @@ -30,6 +30,10 @@ module ClickHouse DatabaseError = Class.new(Error) QueryError = Class.new(Error) + def self.database_configured?(database, configuration = self.configuration) + !!configuration.databases[database] + end + # Executes a SELECT database query def self.select(query, database, configuration = self.configuration) instrumented_execute(query, database, configuration) do |response, instrument| diff --git a/gems/csv_builder/lib/csv_builder/builder.rb b/gems/csv_builder/lib/csv_builder/builder.rb index 99b63153ab2..ff7e51cf7ce 100644 --- a/gems/csv_builder/lib/csv_builder/builder.rb +++ b/gems/csv_builder/lib/csv_builder/builder.rb @@ -78,7 +78,9 @@ module CsvBuilder def row(object) attributes.map do |attribute| - if attribute.respond_to?(:call) + if object.is_a?(Hash) + excel_sanitize(object[attribute]) + elsif attribute.respond_to?(:call) excel_sanitize(attribute.call(object)) else excel_sanitize(object.public_send(attribute)) # rubocop:disable GitlabSecurity/PublicSend diff --git a/lib/gitlab/background_migration/backfill_vs_code_settings_uuid.rb b/lib/gitlab/background_migration/backfill_vs_code_settings_uuid.rb new file mode 100644 index 00000000000..2bb0e0b6d98 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_vs_code_settings_uuid.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class BackfillVsCodeSettingsUuid < BatchedMigrationJob + operation_name :backfill_vs_code_settings_uuid + scope_to ->(relation) { relation.where(uuid: nil) } + feature_category :web_ide + + def perform + each_sub_batch do |sub_batch| + vs_code_settings = sub_batch.map do |vs_code_setting| + vs_code_setting.attributes.merge(uuid: SecureRandom.uuid) + end + + VsCode::Settings::VsCodeSetting.upsert_all(vs_code_settings) + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/auto_cancel.rb b/lib/gitlab/ci/config/entry/auto_cancel.rb index 96f809d40a8..2c51ab82214 100644 --- a/lib/gitlab/ci/config/entry/auto_cancel.rb +++ b/lib/gitlab/ci/config/entry/auto_cancel.rb @@ -8,8 +8,9 @@ module Gitlab include ::Gitlab::Config::Entry::Attributable include ::Gitlab::Config::Entry::Validatable - ALLOWED_KEYS = %i[on_new_commit].freeze + ALLOWED_KEYS = %i[on_new_commit on_job_failure].freeze ALLOWED_ON_NEW_COMMIT_OPTIONS = ::Ci::PipelineMetadata.auto_cancel_on_new_commits.keys.freeze + ALLOWED_ON_JOB_FAILURE_OPTIONS = ::Ci::PipelineMetadata.auto_cancel_on_job_failures.keys.freeze attributes ALLOWED_KEYS @@ -19,6 +20,10 @@ module Gitlab in: ALLOWED_ON_NEW_COMMIT_OPTIONS, message: format(_("must be one of: %{values}"), values: ALLOWED_ON_NEW_COMMIT_OPTIONS.join(', ')) } + validates :on_job_failure, allow_nil: true, type: String, inclusion: { + in: ALLOWED_ON_JOB_FAILURE_OPTIONS, + message: format(_("must be one of: %{values}"), values: ALLOWED_ON_JOB_FAILURE_OPTIONS.join(', ')) + } end end end diff --git a/lib/gitlab/ci/pipeline/chain/populate_metadata.rb b/lib/gitlab/ci/pipeline/chain/populate_metadata.rb index 1d79b7260e1..3ac910da752 100644 --- a/lib/gitlab/ci/pipeline/chain/populate_metadata.rb +++ b/lib/gitlab/ci/pipeline/chain/populate_metadata.rb @@ -36,13 +36,31 @@ module Gitlab def set_auto_cancel auto_cancel = @command.yaml_processor_result.workflow_auto_cancel - auto_cancel_on_new_commit = auto_cancel&.dig(:on_new_commit) + + return if auto_cancel.blank? + + set_auto_cancel_on_new_commit(auto_cancel) + set_auto_cancel_on_job_failure(auto_cancel) + end + + def set_auto_cancel_on_new_commit(auto_cancel) + auto_cancel_on_new_commit = auto_cancel[:on_new_commit] return if auto_cancel_on_new_commit.blank? assign_to_metadata(auto_cancel_on_new_commit: auto_cancel_on_new_commit) end + def set_auto_cancel_on_job_failure(auto_cancel) + return if Feature.disabled?(:auto_cancel_pipeline_on_job_failure, pipeline.project) + + auto_cancel_on_job_failure = auto_cancel[:on_job_failure] + + return if auto_cancel_on_job_failure.blank? + + assign_to_metadata(auto_cancel_on_job_failure: auto_cancel_on_job_failure) + end + def global_context Gitlab::Ci::Build::Context::Global.new( pipeline, yaml_variables: @command.pipeline_seed.root_variables) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 54936b0079c..edd2e11a2e9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25005,6 +25005,9 @@ msgstr "" msgid "Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials." msgstr "" +msgid "Import|This project is being imported. Do not make any changes to the project until the import is complete." +msgstr "" + msgid "Import|Timeout for decompressing archived files (seconds)" msgstr "" diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index f0a73391d5d..2ffd4e5d7af 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -119,7 +119,7 @@ module QA def with_retry_on_too_many_requests response = nil - Support::Retrier.retry_until(log: false) do + Support::Retrier.retry_until(log: false, message: "Retrying upon receiving 429 HTTP status") do response = yield if response.code == HTTP_STATUS_TOO_MANY_REQUESTS diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb index de8fbf7f044..96e9d53cac3 100644 --- a/qa/qa/tools/reliable_report.rb +++ b/qa/qa/tools/reliable_report.rb @@ -263,7 +263,7 @@ module QA rows: group_specs.map do |name, result| [ name_column(name: name, file: result[:file], link: result[:link], - exceptions_and_job_urls: result[:exceptions_and_job_urls], markdown: markdown), + exceptions_and_related_urls: result[:exceptions_and_related_urls], markdown: markdown), *table_params(result.values) ] end @@ -345,12 +345,13 @@ module QA # @param [String] name # @param [String] file # @param [String] link - # @param [Hash] exceptions_and_job_urls + # @param [Hash] exceptions_and_related_urls # @param [Boolean] markdown # @return [String] - def name_column(name:, file:, link:, exceptions_and_job_urls:, markdown: false) + def name_column(name:, file:, link:, exceptions_and_related_urls:, markdown: false) if markdown - return "**Name**: #{name}
**File**: [#{file}](#{link})#{exceptions_markdown(exceptions_and_job_urls)}" + return "**Name**: #{name}
**File**: " \ + "[#{file}](#{link})#{exceptions_markdown(exceptions_and_related_urls)}" end wrapped_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name @@ -359,13 +360,13 @@ module QA # Formatted exceptions with link to job url # - # @param [Hash] exceptions_and_job_urls + # @param [Hash] exceptions_and_related_urls # @return [String] - def exceptions_markdown(exceptions_and_job_urls) - return '' if exceptions_and_job_urls.empty? + def exceptions_markdown(exceptions_and_related_urls) + return '' if exceptions_and_related_urls.empty? - "
**Exceptions**:#{exceptions_and_job_urls.keys.map do |e| - "
- [`#{e.truncate(250).tr('`', "'")}`](#{exceptions_and_job_urls[e]})" + "
**Exceptions**:#{exceptions_and_related_urls.keys.map do |e| + "
- [`#{e.truncate(250).tr('`', "'")}`](#{exceptions_and_related_urls[e]})" end.join('')}" end @@ -398,20 +399,13 @@ module QA failure_rate = (failed.to_f / runs) * 100 - records_with_exception = records.reject { |r| !r.values["failure_exception"] } - - # Since exception is the key in the below hash, only one instance of an occurrence is kept - exceptions_and_job_urls = records_with_exception.to_h do |r| - [r.values["failure_exception"], r.values["job_url"]] - end - result[stage][product_group] ||= {} result[stage][product_group][name] = { file: file, link: link, runs: runs, failed: failed, - exceptions_and_job_urls: exceptions_and_job_urls, + exceptions_and_related_urls: exceptions_and_related_urls(records), failure_rate: failure_rate == 0 ? failure_rate.round(0) : failure_rate.round(2) } end @@ -419,6 +413,19 @@ module QA # rubocop:enable Metrics/AbcSize + # Return hash of exceptions as key and failure_issue or job_url urls as value + # + # @param [Array] records + # @return [Hash] + def exceptions_and_related_urls(records) + records_with_exception = records.reject { |r| !r.values["failure_exception"] } + + # Since exception is the key in the below hash, only one instance of an occurrence is kept + records_with_exception.to_h do |r| + [r.values["failure_exception"], r.values["failure_issue"] || r.values["job_url"]] + end + end + # Check if failure is allowed # # @param [String] failure_exception @@ -465,7 +472,8 @@ module QA ) |> filter(fn: (r) => r["_field"] == "job_url" or r["_field"] == "failure_exception" or - r["_field"] == "id" + r["_field"] == "id" or + r["_field"] == "failure_issue" ) |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value") |> group(columns: ["name"]) diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 84d3199a274..2fe6fd8f325 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -4,3 +4,9 @@ require_relative '../qa' require_relative 'scenario_shared_examples' require_relative('../../jh/qa/spec/spec_helper') if GitlabEdition.jh? + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.max_formatted_output_length = nil + end +end diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb index eca3baffdf0..c8488424259 100644 --- a/qa/spec/tools/reliable_report_spec.rb +++ b/qa/spec/tools/reliable_report_spec.rb @@ -118,7 +118,8 @@ describe QA::Tools::ReliableReport do ) |> filter(fn: (r) => r["_field"] == "job_url" or r["_field"] == "failure_exception" or - r["_field"] == "id" + r["_field"] == "id" or + r["_field"] == "failure_issue" ) |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value") |> group(columns: ["name"]) @@ -151,12 +152,12 @@ describe QA::Tools::ReliableReport do ) end - def name_column(spec_name, exceptions_and_job_urls = {}) - "**Name**: #{spec_name}
**File**: [spec.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb)#{exceptions_markdown(exceptions_and_job_urls)}" + def name_column(spec_name, exceptions_and_related_urls = {}) + "**Name**: #{spec_name}
**File**: [spec.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/specs/features/some/spec.rb)#{exceptions_markdown(exceptions_and_related_urls)}" end - def exceptions_markdown(exceptions_and_job_urls) - exceptions_and_job_urls.empty? ? '' : "
**Exceptions**:
- [`#{failure_message}`](https://job/url)" + def exceptions_markdown(exceptions_and_related_urls) + exceptions_and_related_urls.empty? ? '' : "
**Exceptions**:
- [`#{failure_message}`](https://job/url)" end before do @@ -373,4 +374,59 @@ describe QA::Tools::ReliableReport do %q([Unable to find css "[data-testid=\"user_action_dropdown\"]"]))).to be false end end + + describe "#exceptions_and_related_urls" do + subject(:reliable_report) { described_class.new(14) } + + let(:failure_message) { "This is a failure exception" } + let(:job_url) { "https://example.com/job/url" } + let(:failure_issue_url) { "https://example.com/failure/issue" } + + let(:records) do + [instance_double("InfluxDB2::FluxRecord", values: values)] + end + + context "without failure_exception" do + let(:values) do + { + "failure_exception" => nil, + "job_url" => job_url, + "failure_issue" => failure_issue_url + } + end + + it "returns an empty hash" do + expect(reliable_report.send(:exceptions_and_related_urls, records)).to be_empty + end + + context "with failure_exception" do + context "without failure_issue" do + let(:values) do + { + "failure_exception" => failure_message, + "job_url" => job_url + } + end + + it "returns job_url as value" do + expect(reliable_report.send(:exceptions_and_related_urls, records).values).to eq([job_url]) + end + end + + context "with failure_issue and job_url" do + let(:values) do + { + "failure_exception" => failure_message, + "failure_issue" => failure_issue_url, + "job_url" => job_url + } + end + + it "returns failure_issue as value" do + expect(reliable_report.send(:exceptions_and_related_urls, records).values).to eq([failure_issue_url]) + end + end + end + end + end end diff --git a/spec/features/environments/environments_folder_spec.rb b/spec/features/environments/environments_folder_spec.rb index 9e2932e315f..da9de6fba0c 100644 --- a/spec/features/environments/environments_folder_spec.rb +++ b/spec/features/environments/environments_folder_spec.rb @@ -99,6 +99,30 @@ RSpec.describe 'Environments Folder page', :js, feature_category: :environment_m expect(page).not_to have_content(get_env_name(stopped_env)) end end + + describe 'pagination' do + # rubocop:disable RSpec/FactoryBot/ExcessiveCreateList -- need >20 items to test pagination + let!(:envs) { create_list(:environment, 25, :with_folders, project: project, folder: folder_name) } + + # rubocop:enable RSpec/FactoryBot/ExcessiveCreateList + it 'shows pagination' do + pagination = find('.pagination') + + expect(pagination).to have_content('2') + end + + it 'can navigate to the next page and updates the url' do + pagination = find('.pagination') + pagination.scroll_to(:bottom) + within(pagination) do + click_link 'Next' + end + + wait_for_requests + + expect(current_url).to include('page=2') + end + end end describe 'legacy folders page' do diff --git a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap index d7364e80542..33a268c06cc 100644 --- a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap +++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FindingsDrawer matches the snapshot with detected badge 1`] = ` +exports[`FindingsDrawer General Rendering matches the snapshot with detected badge 1`] = `

  • `; -exports[`FindingsDrawer matches the snapshot with dismissed badge 1`] = ` +exports[`FindingsDrawer General Rendering matches the snapshot with dismissed badge 1`] = `

  • - dismissed + detected

  • diff --git a/spec/frontend/diffs/components/shared/findings_drawer_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_spec.js index a6528978e3b..00b4ca262be 100644 --- a/spec/frontend/diffs/components/shared/findings_drawer_spec.js +++ b/spec/frontend/diffs/components/shared/findings_drawer_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import { GlDrawer } from '@gitlab/ui'; import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -5,41 +6,101 @@ import { mockFindingDismissed, mockFindingDetected, mockProject, + mockFindingsMultiple, } from '../../mock_data/findings_drawer'; -let wrapper; -const getDrawer = () => wrapper.findComponent(GlDrawer); -const closeEvent = 'close'; - -const createWrapper = (finding = mockFindingDismissed) => { - return mountExtended(FindingsDrawer, { - propsData: { - drawer: finding, - project: mockProject, - }, - }); -}; - describe('FindingsDrawer', () => { - it('renders without errors', () => { - wrapper = createWrapper(); - expect(wrapper.exists()).toBe(true); + let wrapper; + + const findPreviousButton = () => wrapper.findByTestId('findings-drawer-prev-button'); + const findNextButton = () => wrapper.findByTestId('findings-drawer-next-button'); + const findTitle = () => wrapper.findByTestId('findings-drawer-title'); + const createWrapper = ( + drawer = { findings: [mockFindingDetected], index: 0 }, + project = mockProject, + ) => { + return mountExtended(FindingsDrawer, { + propsData: { + drawer, + project, + }, + }); + }; + + describe('General Rendering', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + it('renders without errors', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('emits close event when gl-drawer emits close event', () => { + wrapper.findComponent(GlDrawer).vm.$emit('close'); + expect(wrapper.emitted('close')).toHaveLength(1); + }); + + it('matches the snapshot with dismissed badge', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('matches the snapshot with detected badge', () => { + expect(wrapper.element).toMatchSnapshot(); + }); }); - it('emits close event when gl-drawer emits close event', () => { - wrapper = createWrapper(); + describe('Prev/Next Buttons with Multiple Items', () => { + it('renders prev/next buttons when there are multiple items', () => { + wrapper = createWrapper({ findings: mockFindingsMultiple, index: 0 }); + expect(findPreviousButton().exists()).toBe(true); + expect(findNextButton().exists()).toBe(true); + }); - getDrawer().vm.$emit(closeEvent); - expect(wrapper.emitted(closeEvent)).toHaveLength(1); + it('does not render prev/next buttons when there is only one item', () => { + wrapper = createWrapper({ findings: [mockFindingDismissed], index: 0 }); + expect(findPreviousButton().exists()).toBe(false); + expect(findNextButton().exists()).toBe(false); + }); + + it('calls prev method on prev button click and loops correct activeIndex', async () => { + wrapper = createWrapper({ findings: mockFindingsMultiple, index: 0 }); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[0].title}`); + + await findPreviousButton().trigger('click'); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[2].title}`); + + await findPreviousButton().trigger('click'); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[1].title}`); + }); + + it('calls next method on next button click', async () => { + wrapper = createWrapper({ findings: mockFindingsMultiple, index: 0 }); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[0].title}`); + + await findNextButton().trigger('click'); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[1].title}`); + + await findNextButton().trigger('click'); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[2].title}`); + + await findNextButton().trigger('click'); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[0].title}`); + }); }); - it('matches the snapshot with dismissed badge', () => { - wrapper = createWrapper(); - expect(wrapper.element).toMatchSnapshot(); - }); + describe('Active Index Handling', () => { + it('watcher sets active index on drawer prop change', async () => { + wrapper = createWrapper(); + const newFinding = { findings: mockFindingsMultiple, index: 2 }; - it('matches the snapshot with detected badge', () => { - wrapper = createWrapper(mockFindingDetected); - expect(wrapper.element).toMatchSnapshot(); + await wrapper.setProps({ drawer: newFinding }); + await nextTick(); + expect(findTitle().text()).toBe(`Name ${mockFindingsMultiple[2].title}`); + }); }); }); diff --git a/spec/frontend/diffs/mock_data/findings_drawer.js b/spec/frontend/diffs/mock_data/findings_drawer.js index f6fc33b1dcf..257a3b3e499 100644 --- a/spec/frontend/diffs/mock_data/findings_drawer.js +++ b/spec/frontend/diffs/mock_data/findings_drawer.js @@ -31,3 +31,45 @@ export const mockProject = { nameWithNamespace: 'testname', fullPath: 'testpath', }; + +export const mockFindingsMultiple = [ + { + ...mockFindingDismissed, + title: 'Finding 1', + severity: 'critical', + engineName: 'Engine 1', + identifiers: [ + { + ...mockFindingDismissed.identifiers[0], + name: 'identifier 1', + url: 'https://example.com/identifier1', + }, + ], + }, + { + ...mockFindingDetected, + title: 'Finding 2', + severity: 'medium', + engineName: 'Engine 2', + identifiers: [ + { + ...mockFindingDetected.identifiers[0], + name: 'identifier 2', + url: 'https://example.com/identifier2', + }, + ], + }, + { + ...mockFindingDetected, + title: 'Finding 3', + severity: 'medium', + engineName: 'Engine 3', + identifiers: [ + { + ...mockFindingDetected.identifiers[0], + name: 'identifier 3', + url: 'https://example.com/identifier3', + }, + ], + }, +]; diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index 8e7f6e8e00a..7986509074e 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -38,6 +38,8 @@ import SecretsYaml from './yaml_tests/positive_tests/secrets.yml'; import ServicesYaml from './yaml_tests/positive_tests/services.yml'; import NeedsParallelMatrixYaml from './yaml_tests/positive_tests/needs_parallel_matrix.yml'; import ScriptYaml from './yaml_tests/positive_tests/script.yml'; +import AutoCancelPipelineOnJobFailureAllYaml from './yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml'; +import AutoCancelPipelineOnJobFailureNoneYaml from './yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml'; // YAML NEGATIVE TEST import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; @@ -64,6 +66,7 @@ import NeedsParallelMatrixNumericYaml from './yaml_tests/negative_tests/needs/pa import NeedsParallelMatrixWrongParallelValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_parallel_value.yml'; import NeedsParallelMatrixWrongMatrixValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_matrix_value.yml'; import ScriptNegativeYaml from './yaml_tests/negative_tests/script.yml'; +import AutoCancelPipelineNegativeYaml from './yaml_tests/negative_tests/auto_cancel_pipeline.yml'; const ajv = new Ajv({ strictTypes: false, @@ -107,6 +110,8 @@ describe('positive tests', () => { SecretsYaml, NeedsParallelMatrixYaml, ScriptYaml, + AutoCancelPipelineOnJobFailureAllYaml, + AutoCancelPipelineOnJobFailureNoneYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a @@ -152,6 +157,7 @@ describe('negative tests', () => { NeedsParallelMatrixWrongParallelValueYaml, NeedsParallelMatrixWrongMatrixValueYaml, ScriptNegativeYaml, + AutoCancelPipelineNegativeYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml new file mode 100644 index 00000000000..0ba3e5632e3 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml @@ -0,0 +1,4 @@ +# invalid workflow:auto-cancel:on-job-failure +workflow: + auto_cancel: + on_job_failure: unexpected_value diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml new file mode 100644 index 00000000000..bf84ff16f42 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml @@ -0,0 +1,4 @@ +# valid workflow:auto-cancel:on-job-failure +workflow: + auto_cancel: + on_job_failure: all diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml new file mode 100644 index 00000000000..b99eb50e962 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml @@ -0,0 +1,4 @@ +# valid workflow:auto-cancel:on-job-failure +workflow: + auto_cancel: + on_job_failure: none diff --git a/spec/frontend/environments/folder/environments_folder_app_spec.js b/spec/frontend/environments/folder/environments_folder_app_spec.js index fbb252fb152..0b76a74e3a0 100644 --- a/spec/frontend/environments/folder/environments_folder_app_spec.js +++ b/spec/frontend/environments/folder/environments_folder_app_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlSkeletonLoader, GlTab } from '@gitlab/ui'; +import { GlSkeletonLoader, GlTab, GlPagination } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EnvironmentsFolderAppComponent from '~/environments/folder/environments_folder_app.vue'; import EnvironmentItem from '~/environments/components/new_environment_item.vue'; @@ -62,6 +62,7 @@ describe('EnvironmentsFolderAppComponent', () => { folderName: mockFolderName, folderPath: '/gitlab-org/test-project/-/environments/folder/dev', scope: 'active', + page: 1, }, }); }; @@ -119,5 +120,12 @@ describe('EnvironmentsFolderAppComponent', () => { expect(modal.props().environment).toEqual(resolvedEnvironment); expect(modal.props().weight).toBe(1); }); + + it('should render pagination component', () => { + const pagination = wrapper.findComponent(GlPagination); + + expect(pagination.props().perPage).toBe(20); + expect(pagination.props().totalItems).toBe(2); + }); }); }); diff --git a/spec/frontend/environments/graphql/resolvers/base_spec.js b/spec/frontend/environments/graphql/resolvers/base_spec.js index 244c86fa679..f78146fe48e 100644 --- a/spec/frontend/environments/graphql/resolvers/base_spec.js +++ b/spec/frontend/environments/graphql/resolvers/base_spec.js @@ -131,13 +131,14 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('folder', () => { it('should fetch the folder url passed to it', async () => { mock - .onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '' } }) + .onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '', page: 1 } }) .reply(HTTP_STATUS_OK, folder); const environmentFolder = await mockResolvers.Query.folder(null, { environment: { folderPath: ENDPOINT }, scope: 'available', search: '', + page: 1, }); expect(environmentFolder).toEqual(resolvedFolder); diff --git a/spec/lib/gitlab/background_migration/backfill_vs_code_settings_uuid_spec.rb b/spec/lib/gitlab/background_migration/backfill_vs_code_settings_uuid_spec.rb new file mode 100644 index 00000000000..bf1fce4094e --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_vs_code_settings_uuid_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillVsCodeSettingsUuid, schema: 20231130140901, feature_category: :web_ide do + let!(:vs_code_settings) { table(:vs_code_settings) } + let!(:users) { table(:users) } + + let!(:user) do + users.create!( + email: "test1@example.com", + username: "test1", + notification_email: "test@example.com", + name: "test", + state: "active", + projects_limit: 10) + end + + subject(:migration) do + described_class.new( + start_id: vs_code_setting_one.id, + end_id: vs_code_setting_two.id, + batch_table: :vs_code_settings, + batch_column: :id, + sub_batch_size: 100, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ) + end + + describe "#perform" do + context 'when it finds vs_code_setting rows with empty uuid' do + let(:vs_code_setting_one) do + vs_code_settings.create!(user_id: user.id, setting_type: 'profiles', content: '{}') + end + + let(:vs_code_setting_two) do + vs_code_settings.create!(user_id: user.id, setting_type: 'tasks', content: '{}') + end + + it 'populates uuid column with a generated uuid' do + expect(vs_code_setting_one.uuid).to be_nil + expect(vs_code_setting_two.uuid).to be_nil + + migration.perform + + expect(vs_code_setting_one.reload.uuid).not_to be_nil + expect(vs_code_setting_two.reload.uuid).not_to be_nil + end + end + + context 'when it finds vs_code_setting rows with non-empty uuid' do + let(:vs_code_setting_one) do + vs_code_settings.create!(user_id: user.id, setting_type: 'profiles', content: '{}', uuid: SecureRandom.uuid) + end + + let(:vs_code_setting_two) do + vs_code_settings.create!(user_id: user.id, setting_type: 'tasks', content: '{}') + end + + it 'populates uuid column with a generated uuid' do + expect(vs_code_setting_one.uuid).not_to be_nil + expect(vs_code_setting_two.uuid).to be_nil + + previous_uuid = vs_code_setting_one.uuid + + migration.perform + + expect(vs_code_setting_one.reload.uuid).to eq(previous_uuid) + expect(vs_code_setting_two.reload.uuid).not_to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb index dff96fc6787..bdd66cc00a1 100644 --- a/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb @@ -30,6 +30,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::AutoCancel, feature_category: :pipelin end end + context 'with on_job_failure' do + ['all', 'none', nil].each do |value| + context 'when the `on_job_failure` value is valid' do + let(:config_hash) { { on_job_failure: value } } + + it { is_expected.to be_valid } + + it 'returns value correctly' do + expect(config.value).to eq(on_job_failure: value) + end + end + end + + context 'when on_job_failure is invalid' do + let(:config_hash) do + { on_job_failure: 'invalid' } + end + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(config.errors) + .to include('auto cancel on job failure must be one of: none, all') + end + end + end + context 'with invalid key' do let(:config_hash) do { invalid: 'interruptible' } diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb index 632072534ab..d3ce3ffe641 100644 --- a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb @@ -123,7 +123,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow, feature_category: :pipeline_ let(:workflow_hash) do { auto_cancel: { - on_new_commit: 'interruptible' + on_new_commit: 'interruptible', + on_job_failure: 'none' } } end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb index d6277c91b2d..732748d8c8b 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb @@ -133,9 +133,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: end context 'with auto_cancel' do - let(:config) do - { workflow: { auto_cancel: { on_new_commit: 'interruptible' } }, rspec: { script: 'rspec' } } - end + let(:on_new_commit) { 'interruptible' } + let(:on_job_failure) { 'all' } + let(:auto_cancel) { { on_new_commit: on_new_commit, on_job_failure: on_job_failure } } + let(:config) { { workflow: { auto_cancel: auto_cancel }, rspec: { script: 'rspec' } } } it_behaves_like 'not breaking the chain' @@ -143,6 +144,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: run_chain expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') expect(pipeline.pipeline_metadata).not_to be_persisted end @@ -155,20 +157,89 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: end context 'with auto_cancel: nil' do - let(:config) do - { workflow: { auto_cancel: nil }, rspec: { script: 'rspec' } } - end + let(:auto_cancel) { nil } it_behaves_like 'not saving pipeline metadata' end - context 'with auto_cancel_on_new_commit: nil' do - let(:config) do - { workflow: { auto_cancel: { on_new_commit: nil } }, rspec: { script: 'rspec' } } + context 'with auto_cancel_on_new_commit and no auto_cancel_on_job_failure' do + let(:auto_cancel) { { on_new_commit: on_new_commit } } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted end + end + + context 'with auto_cancel_on_job_failure and no auto_cancel_on_new_commit' do + let(:auto_cancel) { { on_job_failure: on_job_failure } } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('conservative') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'with auto_cancel_on_new_commit: nil and auto_cancel_on_job_failure: nil' do + let(:on_new_commit) { nil } + let(:on_job_failure) { nil } it_behaves_like 'not saving pipeline metadata' end + + context 'with auto_cancel_on_new_commit valid and auto_cancel_on_job_failure: nil' do + let(:on_job_failure) { nil } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'with auto_cancel_on_new_commit: nil and auto_cancel_on_job_failure valid' do + let(:on_new_commit) { nil } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('conservative') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'when auto_cancel_on_job_failure: none' do + let(:on_job_failure) { 'none' } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'when auto_cancel_pipeline_on_job_failure feature is disabled' do + before do + stub_feature_flags(auto_cancel_pipeline_on_job_failure: false) + end + + it 'ignores the auto_cancel_on_job_failure value' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end end context 'with both pipeline name and auto_cancel' do @@ -176,7 +247,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: { workflow: { name: 'Pipeline name', - auto_cancel: { on_new_commit: 'interruptible' } + auto_cancel: { + on_new_commit: 'interruptible', + on_job_failure: 'none' + } }, rspec: { script: 'rspec' } } @@ -189,6 +263,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: expect(pipeline.pipeline_metadata.name).to eq('Pipeline name') expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') expect(pipeline.pipeline_metadata).not_to be_persisted end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 3ba62888b7c..844a6849c8f 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -502,6 +502,7 @@ module Gitlab workflow: auto_cancel: on_new_commit: interruptible + on_job_failure: all hello: script: echo world @@ -509,7 +510,10 @@ module Gitlab end it 'parses the workflow:auto_cancel as workflow_auto_cancel' do - expect(subject.workflow_auto_cancel).to eq(on_new_commit: 'interruptible') + expect(subject.workflow_auto_cancel).to eq({ + on_new_commit: 'interruptible', + on_job_failure: 'all' + }) end end end diff --git a/spec/migrations/20231130140901_queue_backfill_vs_code_settings_uuid_spec.rb b/spec/migrations/20231130140901_queue_backfill_vs_code_settings_uuid_spec.rb new file mode 100644 index 00000000000..3e697d6b1f3 --- /dev/null +++ b/spec/migrations/20231130140901_queue_backfill_vs_code_settings_uuid_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillVsCodeSettingsUuid, feature_category: :web_ide do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :vs_code_settings, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4a71b34391c..18c7e57d464 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -987,24 +987,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def describe '#artifacts_public?' do subject { build.artifacts_public? } - context 'when non_public_artifacts flag is disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - context 'artifacts with defaults - public' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - it { is_expected.to be_truthy } - end - - context 'non public artifacts' do - let(:build) { create(:ci_build, :private_artifacts, pipeline: pipeline) } - - it { is_expected.to be_truthy } - end - end - context 'artifacts with defaults - public' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -1027,30 +1009,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def describe '#artifact_is_public_in_config?' do subject { build.artifact_is_public_in_config? } - context 'when non_public_artifacts flag is disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - context 'artifacts with defaults' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - it { is_expected.to be_truthy } - end - - context 'non public artifacts' do - let(:build) { create(:ci_build, :with_private_artifacts_config, pipeline: pipeline) } - - it { is_expected.to be_truthy } - end - - context 'public artifacts' do - let(:build) { create(:ci_build, :with_public_artifacts_config, pipeline: pipeline) } - - it { is_expected.to be_truthy } - end - end - context 'artifacts with defaults' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -5270,7 +5228,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def describe '.with_project_and_metadata' do it 'does not join across databases' do with_cross_joins_prevented do - ::Ci::Build.with_project_and_metadata(project).to_a + ::Ci::Build.with_project_and_metadata.to_a end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 48d46824c11..e65c1e2f577 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -176,16 +176,6 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do let!(:artifact) { build(:ci_job_artifact, :private) } it { is_expected.to be_falsey } - - context 'and the non_public_artifacts feature flag is disabled' do - let!(:artifact) { build(:ci_job_artifact, :private) } - - before do - stub_feature_flags(non_public_artifacts: false) - end - - it { is_expected.to be_truthy } - end end end diff --git a/spec/models/ci/pipeline_metadata_spec.rb b/spec/models/ci/pipeline_metadata_spec.rb index 2db7a2c23e9..1a426118063 100644 --- a/spec/models/ci/pipeline_metadata_spec.rb +++ b/spec/models/ci/pipeline_metadata_spec.rb @@ -18,5 +18,13 @@ RSpec.describe Ci::PipelineMetadata, feature_category: :pipeline_composition do conservative: 0, interruptible: 1, disabled: 2 ).with_prefix end + + it do + is_expected.to define_enum_for( + :auto_cancel_on_job_failure + ).with_values( + none: 0, all: 1 + ).with_prefix + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c03ad704cb8..9f504833494 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -86,6 +86,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: it { is_expected.to respond_to :short_sha } it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } it { is_expected.to delegate_method(:name).to(:pipeline_metadata).allow_nil } + it { is_expected.to delegate_method(:auto_cancel_on_job_failure).to(:pipeline_metadata).allow_nil } describe 'validations' do it { is_expected.to validate_presence_of(:sha) } diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index 9fb3f7e85ba..a8c09a5191d 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -197,21 +197,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do expect(response).to have_gitlab_http_status(:forbidden) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts' do - project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) - project.update_column(:public_builds, true) - - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end end context 'when project is public with builds access disabled' do @@ -445,17 +430,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do it 'rejects access and hides existence of artifacts' do expect(response).to have_gitlab_http_status(:forbidden) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) - end - - it 'allows access to artifacts' do - expect(response).to have_gitlab_http_status(:ok) - end - end end it 'does not return job artifacts if not uploaded' do @@ -651,18 +625,6 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do expect(response.headers.to_h) .not_to include('Gitlab-Workhorse-Send-Data' => /artifacts-entry/) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'allows access to artifacts', :sidekiq_might_not_need_inline do - get_artifact_file(artifact) - - expect(response).to have_gitlab_http_status(:ok) - end - end end context 'when project is private' do diff --git a/spec/requests/projects/merge_requests/content_spec.rb b/spec/requests/projects/merge_requests/content_spec.rb index 54066756f3e..1ecad609416 100644 --- a/spec/requests/projects/merge_requests/content_spec.rb +++ b/spec/requests/projects/merge_requests/content_spec.rb @@ -29,13 +29,5 @@ RSpec.describe 'merge request content spec', feature_category: :code_review_work describe 'GET cached_widget' do it_behaves_like 'cached widget request' - - context 'with non_public_artifacts disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it_behaves_like 'cached widget request' - end end end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 9ef1e91ebcd..a899a798fa0 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -286,16 +286,6 @@ RSpec.describe BuildDetailsEntity do it 'does not expose non public artifacts' do expect(subject.keys).not_to include(:artifact) end - - context 'with the non_public_artifacts feature flag disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it 'exposes artifact details' do - expect(subject[:artifact].keys).to include(:download_path, :browse_path, :locked) - end - end end end diff --git a/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb b/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb index 9737b85d654..851c6f8fbea 100644 --- a/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb +++ b/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb @@ -14,49 +14,156 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes stub_ci_pipeline_yaml_file(config) end - context 'when on_new_commit is set to interruptible' do - let(:config) do - <<~YAML - workflow: - auto_cancel: - on_new_commit: interruptible + describe 'on_new_commit' do + context 'when is set to interruptible' do + let(:config) do + <<~YAML + workflow: + auto_cancel: + on_new_commit: interruptible - test1: - script: exit 0 - YAML + test1: + script: exit 0 + YAML + end + + before do + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a pipeline with on_new_commit' do + expect(pipeline).to be_persisted + expect(pipeline.errors).to be_empty + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + end end - before do - stub_ci_pipeline_yaml_file(config) - end + context 'when is set to invalid' do + let(:config) do + <<~YAML + workflow: + auto_cancel: + on_new_commit: invalid - it 'creates a pipeline with on_new_commit' do - expect(pipeline).to be_persisted - expect(pipeline.errors).to be_empty - expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + test1: + script: exit 0 + YAML + end + + before do + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a pipeline with errors' do + expect(pipeline).to be_persisted + expect(pipeline.errors.full_messages).to include( + 'workflow:auto_cancel on new commit must be one of: conservative, interruptible, disabled') + end end end - context 'when on_new_commit is set to invalid' do - let(:config) do - <<~YAML - workflow: - auto_cancel: - on_new_commit: invalid + describe 'on_job_failure' do + context 'when is set to none' do + let(:config) do + <<~YAML + workflow: + auto_cancel: + on_job_failure: none - test1: - script: exit 0 - YAML + test1: + script: exit 0 + YAML + end + + before do + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a pipeline with on_job_failure' do + expect(pipeline).to be_persisted + expect(pipeline.errors).to be_empty + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + end end - before do - stub_ci_pipeline_yaml_file(config) + context 'when is set to all' do + let(:config) do + <<~YAML + workflow: + auto_cancel: + on_job_failure: all + + test1: + script: exit 0 + YAML + end + + before do + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a pipeline with on_job_failure' do + expect(pipeline).to be_persisted + expect(pipeline.errors).to be_empty + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + end + + context 'when auto_cancel_pipeline_on_job_failure feature flag is disabled' do + before do + stub_feature_flags(auto_cancel_pipeline_on_job_failure: false) + end + + context 'when there are no other metadata settings present' do + it 'creates a pipeline without metadata' do + expect(pipeline).to be_persisted + expect(pipeline.errors).to be_empty + expect(pipeline.pipeline_metadata).to be_nil + end + end + + context 'when other metadata settings are present' do + let(:config) do + <<~YAML + workflow: + name: pipeline_name + auto_cancel: + on_job_failure: all + + test1: + script: exit 0 + YAML + end + + it 'creates a pipeline with on_job_failure' do + expect(pipeline).to be_persisted + expect(pipeline.errors).to be_empty + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + end + end + end end - it 'creates a pipeline with errors' do - expect(pipeline).to be_persisted - expect(pipeline.errors.full_messages).to include( - 'workflow:auto_cancel on new commit must be one of: conservative, interruptible, disabled') + context 'when on_job_failure is set to invalid' do + let(:config) do + <<~YAML + workflow: + auto_cancel: + on_job_failure: invalid + + test1: + script: exit 0 + YAML + end + + before do + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a pipeline with errors' do + expect(pipeline).to be_persisted + expect(pipeline.errors.full_messages).to include( + 'workflow:auto_cancel on job failure must be one of: none, all') + end end end end diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index ffb6bd9d145..0d6a15b0ea3 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -155,52 +155,6 @@ RSpec.describe Ci::JobArtifacts::CreateService, :clean_gitlab_redis_shared_state end end - context 'when non_public_artifacts flag is disabled' do - before do - stub_feature_flags(non_public_artifacts: false) - end - - it_behaves_like 'public accessibility' - end - - context 'when non_public_artifacts flag is enabled' do - context 'and accessibility is defined in the params' do - context 'and is passed as private' do - before do - params.merge!('accessibility' => 'private') - end - - it_behaves_like 'private accessibility' - end - - context 'and is passed as public' do - before do - params.merge!('accessibility' => 'public') - end - - it_behaves_like 'public accessibility' - end - end - - context 'and accessibility is not defined in the params' do - context 'and job has no public artifacts defined in its CI config' do - it_behaves_like 'public accessibility' - end - - context 'and job artifacts defined as private in the CI config' do - let(:job) { create(:ci_build, :with_private_artifacts_config, project: project) } - - it_behaves_like 'private accessibility' - end - - context 'and job artifacts defined as public in the CI config' do - let(:job) { create(:ci_build, :with_public_artifacts_config, project: project) } - - it_behaves_like 'public accessibility' - end - end - end - context 'when accessibility passed as invalid value' do before do params.merge!('accessibility' => 'foo') diff --git a/spec/workers/click_house/events_sync_worker_spec.rb b/spec/workers/click_house/events_sync_worker_spec.rb index da74e5e376d..9662f26115a 100644 --- a/spec/workers/click_house/events_sync_worker_spec.rb +++ b/spec/workers/click_house/events_sync_worker_spec.rb @@ -151,7 +151,7 @@ RSpec.describe ClickHouse::EventsSyncWorker, feature_category: :value_stream_man context 'when clickhouse is not configured' do before do - allow(ClickHouse::Client.configuration).to receive(:databases).and_return({}) + allow(ClickHouse::Client).to receive(:database_configured?).and_return(false) end it 'skips execution' do @@ -165,7 +165,7 @@ RSpec.describe ClickHouse::EventsSyncWorker, feature_category: :value_stream_man context 'when exclusive lease error happens' do it 'skips execution' do stub_feature_flags(event_sync_worker_for_click_house: true) - allow(ClickHouse::Client.configuration).to receive(:databases).and_return({ main: :some_db }) + allow(ClickHouse::Client).to receive(:database_configured?).with(:main).and_return(true) expect(worker).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :skipped })