diff options
29 files changed, 524 insertions, 136 deletions
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 3d2eaebf1cb..1a539aa0876 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -6,19 +6,21 @@ import { __, sprintf } from '~/locale'; import TitleField from '~/vue_shared/components/form/title.vue'; import { redirectTo, joinPaths } from '~/lib/utils/url_utility'; import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; +import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants'; import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql'; import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql'; import { getSnippetMixin } from '../mixins/snippets'; import { - SNIPPET_VISIBILITY_PRIVATE, SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR, + SNIPPET_VISIBILITY_PRIVATE, } from '../constants'; +import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql'; + import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetVisibilityEdit from './snippet_visibility_edit.vue'; import SnippetDescriptionEdit from './snippet_description_edit.vue'; -import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants'; export default { components: { @@ -31,6 +33,15 @@ export default { GlLoadingIcon, }, mixins: [getSnippetMixin], + apollo: { + defaultVisibility: { + query: defaultVisibilityQuery, + manual: true, + result({ data: { selectedLevel } }) { + this.selectedLevelDefault = selectedLevel; + }, + }, + }, props: { markdownPreviewPath: { type: String, @@ -56,6 +67,7 @@ export default { isUpdating: false, newSnippet: false, actions: [], + selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE, }; }, computed: { @@ -98,6 +110,13 @@ export default { descriptionFieldId() { return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`; }, + newSnippetSchema() { + return { + title: '', + description: '', + visibilityLevel: this.selectedLevelDefault, + }; + }, }, beforeCreate() { performance.mark(SNIPPET_MARK_EDIT_APP_START); @@ -126,7 +145,7 @@ export default { }, onNewSnippetFetched() { this.newSnippet = true; - this.snippet = this.$options.newSnippetSchema; + this.snippet = this.newSnippetSchema; }, onExistingSnippetFetched() { this.newSnippet = false; @@ -184,11 +203,6 @@ export default { this.actions = actions; }, }, - newSnippetSchema: { - title: '', - description: '', - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, - }, }; </script> <template> diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue index 299bb8fcfad..25ad7c214b2 100644 --- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue @@ -1,11 +1,8 @@ <script> import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; -import { - SNIPPET_VISIBILITY, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; +import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql'; +import { defaultSnippetVisibilityLevels } from '../utils/blob'; +import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants'; export default { components: { @@ -15,6 +12,16 @@ export default { GlFormRadioGroup, GlLink, }, + apollo: { + defaultVisibility: { + query: defaultVisibilityQuery, + manual: true, + result({ data: { visibilityLevels, multipleLevelsRestricted } }) { + this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels); + this.multipleLevelsRestricted = multipleLevelsRestricted; + }, + }, + }, props: { helpLink: { type: String, @@ -28,19 +35,17 @@ export default { }, value: { type: String, - required: false, - default: SNIPPET_VISIBILITY_PRIVATE, + required: true, }, }, - computed: { - visibilityOptions() { - return [ - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, - ].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] })); - }, + data() { + return { + visibilityLevels: [], + multipleLevelsRestricted: false, + }; }, + SNIPPET_LEVELS_DISABLED, + SNIPPET_LEVELS_RESTRICTED, }; </script> <template> @@ -51,10 +56,10 @@ export default { ><gl-icon :size="12" name="question" /></gl-link> </label> - <gl-form-group id="visibility-level-setting"> - <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners"> + <gl-form-group id="visibility-level-setting" class="gl-mb-0"> + <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners"> <gl-form-radio - v-for="option in visibilityOptions" + v-for="option in visibilityLevels" :key="option.value" :value="option.value" class="mb-3" @@ -71,5 +76,12 @@ export default { </gl-form-radio> </gl-form-radio-group> </gl-form-group> + + <div class="text-muted" data-testid="restricted-levels-info"> + <template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template> + <template v-else-if="multipleLevelsRestricted">{{ + $options.SNIPPET_LEVELS_RESTRICTED + }}</template> + </div> </div> </template> diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js index 12b83525bf7..e75922df15f 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move'; export const SNIPPET_BLOB_ACTION_DELETE = 'delete'; export const SNIPPET_MAX_BLOBS = 10; + +export const SNIPPET_LEVELS_MAP = { + 0: SNIPPET_VISIBILITY_PRIVATE, + 10: SNIPPET_VISIBILITY_INTERNAL, + 20: SNIPPET_VISIBILITY_PUBLIC, +}; +export const SNIPPET_LEVELS_RESTRICTED = __( + 'Other visibility settings have been disabled by the administrator.', +); +export const SNIPPET_LEVELS_DISABLED = __( + 'Visibility settings have been disabled by the administrator.', +); diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js index bb5e7d6e3f0..c70ad9b95f8 100644 --- a/app/assets/javascripts/snippets/index.js +++ b/app/assets/javascripts/snippets/index.js @@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql'; import SnippetsShow from './components/show.vue'; import SnippetsEdit from './components/edit.vue'; +import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants'; Vue.use(VueApollo); Vue.use(Translate); @@ -18,13 +19,28 @@ function appFactory(el, Component) { defaultClient: createDefaultClient(), }); + const { + visibilityLevels = '[]', + selectedLevel, + multipleLevelsRestricted, + ...restDataset + } = el.dataset; + + apolloProvider.clients.defaultClient.cache.writeData({ + data: { + visibilityLevels: JSON.parse(visibilityLevels), + selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE, + multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset, + }, + }); + return new Vue({ el, apolloProvider, render(createElement) { return createElement(Component, { props: { - ...el.dataset, + ...restDataset, }, }); }, diff --git a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql new file mode 100644 index 00000000000..5bd6c131bab --- /dev/null +++ b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql @@ -0,0 +1,5 @@ +query defaultSnippetVisibility { + visibilityLevels @client + selectedLevel @client + multipleLevelsRestricted @client +} diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js index fd5ff9a3d2e..21f52671801 100644 --- a/app/assets/javascripts/snippets/utils/blob.js +++ b/app/assets/javascripts/snippets/utils/blob.js @@ -4,6 +4,8 @@ import { SNIPPET_BLOB_ACTION_UPDATE, SNIPPET_BLOB_ACTION_MOVE, SNIPPET_BLOB_ACTION_DELETE, + SNIPPET_LEVELS_MAP, + SNIPPET_VISIBILITY, } from '../constants'; const createLocalId = () => uniqueId('blob_local_'); @@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => { return [...deletedEntries, ...newEntries]; }; + +export const defaultSnippetVisibilityLevels = arr => { + if (Array.isArray(arr)) { + return arr.map(l => { + const translatedLevel = SNIPPET_LEVELS_MAP[l]; + return { + value: translatedLevel, + ...SNIPPET_VISIBILITY[translatedLevel], + }; + }); + } + return []; +}; diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index 3d8823728e7..6e9b8416c10 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -33,8 +33,6 @@ module Ci state :still_failing, value: 5 after_transition any => [:fixed, :success] do |ci_ref| - next unless ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(ci_ref.project) - ci_ref.run_after_commit do Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id) end diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb index 2ac1de4321a..73f8fe77b04 100644 --- a/app/models/merge_request_assignee.rb +++ b/app/models/merge_request_assignee.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class MergeRequestAssignee < ApplicationRecord - belongs_to :merge_request + belongs_to :merge_request, touch: true belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees validates :assignee, uniqueness: { scope: :merge_request_id } diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb index 5694d031a0f..ca6e60f819a 100644 --- a/app/services/ci/destroy_expired_job_artifacts_service.rb +++ b/app/services/ci/destroy_expired_job_artifacts_service.rb @@ -28,7 +28,7 @@ module Ci private def destroy_batch(klass) - artifact_batch = if klass == Ci::JobArtifact && Gitlab::Ci::Features.destroy_only_unlocked_expired_artifacts_enabled? + artifact_batch = if klass == Ci::JobArtifact klass.expired(BATCH_SIZE).unlocked else klass.expired(BATCH_SIZE) diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 81277b50d13..41098b60193 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,5 +1,6 @@ - if Feature.enabled?(:snippets_edit_vue) - #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } } + - available_visibility_levels = available_visibility_levels(@snippet) + #js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } } - else .snippet-form-holder = form_for @snippet, url: url, diff --git a/changelogs/unreleased/mc-feature-remove-keep-latest-artifact-ff.yml b/changelogs/unreleased/mc-feature-remove-keep-latest-artifact-ff.yml new file mode 100644 index 00000000000..8b3449f3273 --- /dev/null +++ b/changelogs/unreleased/mc-feature-remove-keep-latest-artifact-ff.yml @@ -0,0 +1,5 @@ +--- +title: Remove keep latest artifact feature flags. +merge_request: 40478 +author: +type: other diff --git a/changelogs/unreleased/pherlihy-master-patch-95213.yml b/changelogs/unreleased/pherlihy-master-patch-95213.yml new file mode 100644 index 00000000000..8d129388d47 --- /dev/null +++ b/changelogs/unreleased/pherlihy-master-patch-95213.yml @@ -0,0 +1,5 @@ +--- +title: Change merge request updated_at when assignees are changed +merge_request: 41030 +author: Patrick Herlihy +type: fixed diff --git a/db/migrate/20200826073745_add_default_to_ci_pipeline_locked.rb b/db/migrate/20200826073745_add_default_to_ci_pipeline_locked.rb new file mode 100644 index 00000000000..e73963043d5 --- /dev/null +++ b/db/migrate/20200826073745_add_default_to_ci_pipeline_locked.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AddDefaultToCiPipelineLocked < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + ARTIFACTS_LOCKED = 1 + UNLOCKED = 0 + + def up + with_lock_retries do + change_column_default :ci_pipelines, :locked, ARTIFACTS_LOCKED + end + end + + def down + with_lock_retries do + change_column_default :ci_pipelines, :locked, UNLOCKED + end + end +end diff --git a/db/post_migrate/20200826121552_remove_ci_job_artifacts_locked.rb b/db/post_migrate/20200826121552_remove_ci_job_artifacts_locked.rb new file mode 100644 index 00000000000..21c7acca7ac --- /dev/null +++ b/db/post_migrate/20200826121552_remove_ci_job_artifacts_locked.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class RemoveCiJobArtifactsLocked < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + remove_column :ci_job_artifacts, :locked + end + end + + def down + with_lock_retries do + add_column :ci_job_artifacts, :locked, :boolean + end + end +end diff --git a/db/schema_migrations/20200826073745 b/db/schema_migrations/20200826073745 new file mode 100644 index 00000000000..0200c7bc66a --- /dev/null +++ b/db/schema_migrations/20200826073745 @@ -0,0 +1 @@ +d3b15469120ed213363de33a4b268ed71a710c40f02d4a669edf2c5412907209
\ No newline at end of file diff --git a/db/schema_migrations/20200826121552 b/db/schema_migrations/20200826121552 new file mode 100644 index 00000000000..e794680240a --- /dev/null +++ b/db/schema_migrations/20200826121552 @@ -0,0 +1 @@ +8667c30042b19428b97e0995821c183e69f73394503c83a55ba7bd870df7c3e8
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 754fbac8dd1..826ffe6e9f5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10083,7 +10083,6 @@ CREATE TABLE public.ci_job_artifacts ( file_sha256 bytea, file_format smallint, file_location smallint, - locked boolean, CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL)) ); @@ -10267,7 +10266,7 @@ CREATE TABLE public.ci_pipelines ( target_sha bytea, external_pull_request_id bigint, ci_ref_id bigint, - locked smallint DEFAULT 0 NOT NULL, + locked smallint DEFAULT 1 NOT NULL, CONSTRAINT check_d7e99a025e CHECK ((lock_version IS NOT NULL)) ); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index e1321deea21..c49ff1a5609 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1716,6 +1716,26 @@ type ClusterAgent { } """ +The connection type for ClusterAgent. +""" +type ClusterAgentConnection { + """ + A list of edges. + """ + edges: [ClusterAgentEdge] + + """ + A list of nodes. + """ + nodes: [ClusterAgent] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" Autogenerated input type of ClusterAgentDelete """ input ClusterAgentDeleteInput { @@ -1745,6 +1765,21 @@ type ClusterAgentDeletePayload { errors: [String!]! } +""" +An edge in a connection. +""" +type ClusterAgentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ClusterAgent +} + type ClusterAgentToken { """ Cluster agent this token is associated with @@ -11319,6 +11354,31 @@ type Project { ): BoardConnection """ + Cluster agents associated with the project + """ + clusterAgents( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ClusterAgentConnection + + """ Compliance frameworks associated with the project """ complianceFrameworks( diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 36bc3f6e83d..e7beae1b01a 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -4682,6 +4682,73 @@ "possibleTypes": null }, { + "kind": "OBJECT", + "name": "ClusterAgentConnection", + "description": "The connection type for ClusterAgent.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ClusterAgentEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ClusterAgent", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { "kind": "INPUT_OBJECT", "name": "ClusterAgentDeleteInput", "description": "Autogenerated input type of ClusterAgentDelete", @@ -4771,6 +4838,51 @@ }, { "kind": "OBJECT", + "name": "ClusterAgentEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ClusterAgent", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "ClusterAgentToken", "description": null, "fields": [ @@ -33856,6 +33968,59 @@ "deprecationReason": null }, { + "name": "clusterAgents", + "description": "Cluster agents associated with the project", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ClusterAgentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "complianceFrameworks", "description": "Compliance frameworks associated with the project", "args": [ diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d18f081dbad..f93d021daf5 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3289,10 +3289,10 @@ job: ``` NOTE: **Note:** -Since [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/16267), the latest -artifacts for refs can be locked against deletion, and kept regardless of the expiry time. This feature is disabled -by default and is not ready for production use. It can be enabled for testing by -enabling the `:keep_latest_artifact_for_ref` and `:destroy_only_unlocked_expired_artifacts` [feature flags](../../administration/feature_flags.md). +The latest artifacts for refs are locked against deletion, and kept regardless of +the expiry time. [Introduced in](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) +GitLab 13.0 behind a disabled feature flag, and [made the default behavior](https://gitlab.com/gitlab-org/gitlab/-/issues/229936) +in GitLab 13.4. #### `artifacts:reports` diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index ab70ba5e17e..a03206c1358 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -40,14 +40,6 @@ module Gitlab ::Feature.enabled?(:ci_raise_job_rules_without_workflow_rules_warning, default_enabled: true) end - def self.keep_latest_artifacts_for_ref_enabled?(project) - ::Feature.enabled?(:keep_latest_artifacts_for_ref, project, default_enabled: true) - end - - def self.destroy_only_unlocked_expired_artifacts_enabled? - ::Feature.enabled?(:destroy_only_unlocked_expired_artifacts, default_enabled: true) - end - def self.bulk_insert_on_create?(project) ::Feature.enabled?(:ci_bulk_insert_on_create, project, default_enabled: true) end diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index 4190c40eb66..9662209f88e 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -20,11 +20,7 @@ module Gitlab pipeline_schedule: @command.schedule, merge_request: @command.merge_request, external_pull_request: @command.external_pull_request, - variables_attributes: Array(@command.variables_attributes), - # This should be removed and set on the database column default - # level when the keep_latest_artifacts_for_ref feature flag is - # removed. - locked: ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(@command.project) ? :artifacts_locked : :unlocked + variables_attributes: Array(@command.variables_attributes) ) end diff --git a/package.json b/package.json index ce902a96581..13620f680cb 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.161.0", - "@gitlab/ui": "20.18.1", + "@gitlab/ui": "20.18.3", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", "@sentry/browser": "^5.22.3", diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb index b6cd97dcc5a..35446b125ce 100644 --- a/spec/features/merge_request/user_assigns_themselves_spec.rb +++ b/spec/features/merge_request/user_assigns_themselves_spec.rb @@ -21,6 +21,10 @@ RSpec.describe 'Merge request > User assigns themselves' do expect(page).to have_content '2 issues have been assigned to you' end + it 'updates updated_by', :js do + expect { click_button 'assign yourself' }.to change { merge_request.reload.updated_at } + end + it 'returns user to the merge request', :js do click_link 'Assign yourself to these issues' diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap index be75a5bfbdc..8446f0f50c4 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap @@ -20,6 +20,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = </label> <gl-form-group-stub + class="gl-mb-0" id="visibility-level-setting" > <gl-form-radio-group-stub @@ -90,5 +91,12 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = </gl-form-radio-stub> </gl-form-radio-group-stub> </gl-form-group-stub> + + <div + class="text-muted" + data-testid="restricted-levels-info" + > + <!----> + </div> </div> `; diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index 33152591d42..b6abb9f389a 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -102,6 +102,13 @@ describe('Snippet Edit app', () => { markdownDocsPath: 'http://docs.foo.bar', ...props, }, + data() { + return { + snippet: { + visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, + }, + }; + }, }); } diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js index 4ba3e906fc3..3919e4d7993 100644 --- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js @@ -1,31 +1,55 @@ import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; +import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob'; import { SNIPPET_VISIBILITY, SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC, + SNIPPET_LEVELS_RESTRICTED, + SNIPPET_LEVELS_DISABLED, } from '~/snippets/constants'; describe('Snippet Visibility Edit component', () => { let wrapper; const defaultHelpLink = '/foo/bar'; const defaultVisibilityLevel = 'private'; - - function createComponent(propsData = {}, deep = false) { + const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]); + + function createComponent({ + propsData = {}, + visibilityLevels = defaultVisibility, + multipleLevelsRestricted = false, + deep = false, + } = {}) { const method = deep ? mount : shallowMount; + const $apollo = { + queries: { + defaultVisibility: { + loading: false, + }, + }, + }; + wrapper = method.call(this, SnippetVisibilityEdit, { + mock: { $apollo }, propsData: { helpLink: defaultHelpLink, isProjectSnippet: false, value: defaultVisibilityLevel, ...propsData, }, + data() { + return { + visibilityLevels, + multipleLevelsRestricted, + }; + }, }); } - const findLabel = () => wrapper.find('label'); + const findLink = () => wrapper.find('label').find(GlLink); const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio); const findRadiosData = () => findRadios().wrappers.map(x => { @@ -47,60 +71,84 @@ describe('Snippet Visibility Edit component', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('renders visibility options', () => { - createComponent({}, true); + it('renders label help link', () => { + createComponent(); + + expect(findLink().attributes('href')).toBe(defaultHelpLink); + }); + + it('when helpLink is not defined, does not render label help link', () => { + createComponent({ propsData: { helpLink: null } }); - expect(findRadiosData()).toEqual([ - { + expect(findLink().exists()).toBe(false); + }); + + describe('Visibility options', () => { + const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]'); + const RESULTING_OPTIONS = { + 0: { value: SNIPPET_VISIBILITY_PRIVATE, icon: SNIPPET_VISIBILITY.private.icon, text: SNIPPET_VISIBILITY.private.label, description: SNIPPET_VISIBILITY.private.description, }, - { + 10: { value: SNIPPET_VISIBILITY_INTERNAL, icon: SNIPPET_VISIBILITY.internal.icon, text: SNIPPET_VISIBILITY.internal.label, description: SNIPPET_VISIBILITY.internal.description, }, - { + 20: { value: SNIPPET_VISIBILITY_PUBLIC, icon: SNIPPET_VISIBILITY.public.icon, text: SNIPPET_VISIBILITY.public.label, description: SNIPPET_VISIBILITY.public.description, }, - ]); - }); - - it('when project snippet, renders special private description', () => { - createComponent({ isProjectSnippet: true }, true); + }; - expect(findRadiosData()[0]).toEqual({ - value: SNIPPET_VISIBILITY_PRIVATE, - icon: SNIPPET_VISIBILITY.private.icon, - text: SNIPPET_VISIBILITY.private.label, - description: SNIPPET_VISIBILITY.private.description_project, + it.each` + levels | resultOptions + ${undefined} | ${[]} + ${''} | ${[]} + ${[]} | ${[]} + ${[0]} | ${[RESULTING_OPTIONS[0]]} + ${[0, 10]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10]]} + ${[0, 10, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]} + ${[0, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[20]]} + ${[10, 20]} | ${[RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]} + `('renders correct visibility options for $levels', ({ levels, resultOptions }) => { + createComponent({ visibilityLevels: defaultSnippetVisibilityLevels(levels), deep: true }); + expect(findRadiosData()).toEqual(resultOptions); }); - }); - it('renders label help link', () => { - createComponent(); - - expect( - findLabel() - .find(GlLink) - .attributes('href'), - ).toBe(defaultHelpLink); - }); + it.each` + levels | levelsRestricted | resultText + ${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED} + ${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED} + ${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED} + ${[0]} | ${false} | ${''} + ${[0, 10, 20]} | ${false} | ${''} + `( + 'renders correct information about restricted visibility levels for $levels', + ({ levels, levelsRestricted, resultText }) => { + createComponent({ + visibilityLevels: defaultSnippetVisibilityLevels(levels), + multipleLevelsRestricted: levelsRestricted, + }); + expect(findRestrictedInfo().text()).toBe(resultText); + }, + ); - it('when helpLink is not defined, does not render label help link', () => { - createComponent({ helpLink: null }); + it('when project snippet, renders special private description', () => { + createComponent({ propsData: { isProjectSnippet: true }, deep: true }); - expect( - findLabel() - .find(GlLink) - .exists(), - ).toBe(false); + expect(findRadiosData()[0]).toEqual({ + value: SNIPPET_VISIBILITY_PRIVATE, + icon: SNIPPET_VISIBILITY.private.icon, + text: SNIPPET_VISIBILITY.private.label, + description: SNIPPET_VISIBILITY.private.description_project, + }); + }); }); }); @@ -108,7 +156,7 @@ describe('Snippet Visibility Edit component', () => { it('pre-selects correct option in the list', () => { const value = SNIPPET_VISIBILITY_INTERNAL; - createComponent({ value }); + createComponent({ propsData: { value } }); expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value); }); diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb index c82c7cb85fa..c9069d82e07 100644 --- a/spec/models/ci/ref_spec.rb +++ b/spec/models/ci/ref_spec.rb @@ -16,51 +16,33 @@ RSpec.describe Ci::Ref do stub_const('Ci::PipelineSuccessUnlockArtifactsWorker', unlock_artifacts_worker_spy) end - context 'when keep latest artifact feature is enabled' do - before do - stub_feature_flags(keep_latest_artifacts_for_ref: true) - end - - where(:initial_state, :action, :count) do - :unknown | :succeed! | 1 - :unknown | :do_fail! | 0 - :success | :succeed! | 1 - :success | :do_fail! | 0 - :failed | :succeed! | 1 - :failed | :do_fail! | 0 - :fixed | :succeed! | 1 - :fixed | :do_fail! | 0 - :broken | :succeed! | 1 - :broken | :do_fail! | 0 - :still_failing | :succeed | 1 - :still_failing | :do_fail | 0 - end - - with_them do - context "when transitioning states" do - before do - status_value = Ci::Ref.state_machines[:status].states[initial_state].value - ci_ref.update!(status: status_value) - end - - it 'calls unlock artifacts service' do - ci_ref.send(action) - - expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times - end - end - end + where(:initial_state, :action, :count) do + :unknown | :succeed! | 1 + :unknown | :do_fail! | 0 + :success | :succeed! | 1 + :success | :do_fail! | 0 + :failed | :succeed! | 1 + :failed | :do_fail! | 0 + :fixed | :succeed! | 1 + :fixed | :do_fail! | 0 + :broken | :succeed! | 1 + :broken | :do_fail! | 0 + :still_failing | :succeed | 1 + :still_failing | :do_fail | 0 end - context 'when keep latest artifact feature is not enabled' do - before do - stub_feature_flags(keep_latest_artifacts_for_ref: false) - end + with_them do + context "when transitioning states" do + before do + status_value = Ci::Ref.state_machines[:status].states[initial_state].value + ci_ref.update!(status: status_value) + end - it 'does not call unlock artifacts service' do - ci_ref.succeed! + it 'calls unlock artifacts service' do + ci_ref.send(action) - expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async) + expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times + end end end end diff --git a/yarn.lock b/yarn.lock index 74b63faa243..ef53ffb3643 100644 --- a/yarn.lock +++ b/yarn.lock @@ -848,10 +848,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.161.0.tgz#661e8d19862dfba0e4c558e2eb6d64b402c1453e" integrity sha512-qsbboEICn08ZoEoAX/TuYygsFaXlzsCY+CfmdOzqvJbOdfHhVXmrJBxd2hP2qqjTZm2PkbRRmn+03+ce1jvatQ== -"@gitlab/ui@20.18.1": - version "20.18.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.18.1.tgz#c8a1e0830b63c056999b9417a499677fd46659af" - integrity sha512-WbLBP6Ni8YxKqlKOZChmedc8uS7MRm5CYg/k3mRUELydF/LoW4/M0CsKwgplW4OJfEQRJr8bvmjiTLAyKAky4g== +"@gitlab/ui@20.18.3": + version "20.18.3" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.18.3.tgz#14b1d6571756fbef7966d6658f56ca213ded15e8" + integrity sha512-yeXc40J8ifXm1aApuNNjEOZraDexYAyKZNRa05rLPXPppLnwZtx4Io5z9QiskN6fMiL648EgzC8JW576qtByCw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" @@ -861,7 +861,7 @@ echarts "^4.2.1" highlight.js "^9.13.1" js-beautify "^1.8.8" - lodash "^4.17.14" + lodash "^4.17.20" portal-vue "^2.1.6" resize-observer-polyfill "^1.5.1" url-search-params-polyfill "^5.0.0" |