diff options
57 files changed, 743 insertions, 294 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ab5047096f3..d7df1264fe3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1125,33 +1125,6 @@ Rails/SaveBang: - 'spec/requests/api/labels_spec.rb' - 'spec/requests/api/project_import_spec.rb' - 'spec/requests/projects/cycle_analytics_events_spec.rb' - - 'spec/services/auth/container_registry_authentication_service_spec.rb' - - 'spec/services/auto_merge/base_service_spec.rb' - - 'spec/services/auto_merge_service_spec.rb' - - 'spec/services/clusters/update_service_spec.rb' - - 'spec/services/deployments/after_create_service_spec.rb' - - 'spec/services/design_management/generate_image_versions_service_spec.rb' - - 'spec/services/discussions/resolve_service_spec.rb' - - 'spec/services/draft_notes/destroy_service_spec.rb' - - 'spec/services/emails/confirm_service_spec.rb' - - 'spec/services/groups/destroy_service_spec.rb' - - 'spec/services/groups/import_export/import_service_spec.rb' - - 'spec/services/labels/promote_service_spec.rb' - - 'spec/services/notes/create_service_spec.rb' - - 'spec/services/notification_recipients/build_service_spec.rb' - - 'spec/services/notification_service_spec.rb' - - 'spec/services/packages/conan/create_package_file_service_spec.rb' - - 'spec/services/reset_project_cache_service_spec.rb' - - 'spec/services/resource_events/change_milestone_service_spec.rb' - - 'spec/services/system_hooks_service_spec.rb' - - 'spec/services/system_note_service_spec.rb' - - 'spec/services/system_notes/issuables_service_spec.rb' - - 'spec/services/todo_service_spec.rb' - - 'spec/services/todos/destroy/confidential_issue_service_spec.rb' - - 'spec/services/users/destroy_service_spec.rb' - - 'spec/services/users/repair_ldap_blocked_service_spec.rb' - - 'spec/services/verify_pages_domain_service_spec.rb' - - 'spec/sidekiq/cron/job_gem_dependency_spec.rb' # Offense count: 187 # Cop supports --auto-correct. @@ -1269,11 +1242,10 @@ RSpec/TimecopTravel: - 'spec/workers/concerns/reenqueuer_spec.rb' - 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb' -# Offense count: 43 +# Offense count: 21 Graphql/IDType: Exclude: - 'ee/app/graphql/ee/mutations/issues/update.rb' - - 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb' - 'ee/app/graphql/mutations/iterations/update.rb' - 'ee/app/graphql/resolvers/iterations_resolver.rb' - 'app/graphql/mutations/boards/issues/issue_move_list.rb' diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js index 51077296e20..6f5aef54cef 100644 --- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { tag: __('Yes or No'), lowercaseValueOnSubmit: true, capitalizeTokenValue: true, + hideNotEqual: true, }, conditions: [ { @@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { value: __('No'), operator: '=', }, - { - url: 'not[wip]=yes', - replacementUrl: 'not[draft]=yes', - tokenKey: 'draft', - value: __('Yes'), - operator: '!=', - }, - { - url: 'not[wip]=no', - replacementUrl: 'not[draft]=no', - tokenKey: 'draft', - value: __('No'), - operator: '!=', - }, ], }; diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js index 1bbd33b6258..8fee3385de1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_operator.js +++ b/app/assets/javascripts/filtered_search/dropdown_operator.js @@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown { this.dispatchInputEvent(); } - renderContent(forceShowList = false) { + renderContent(forceShowList = false, dropdownName = '') { const dropdownData = [ { tag: 'equal', @@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown { help: __('is'), }, ]; + const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase()); - if (gon.features?.notIssuableQueries) { + if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) { dropdownData.push({ tag: 'not-equal', type: 'string', diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index f7ce2ea01e0..8626e1a3d18 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -83,16 +83,16 @@ export default class FilteredSearchDropdown { } } - render(forceRenderContent = false, forceShowList = false) { + render(forceRenderContent = false, forceShowList = false, hideNotEqual = false) { this.setAsDropdown(); const currentHook = this.getCurrentHook(); const firstTimeInitialized = currentHook === null; if (firstTimeInitialized || forceRenderContent) { - this.renderContent(forceShowList); + this.renderContent(forceShowList, hideNotEqual); } else if (currentHook.list.list.id !== this.dropdown.id) { - this.renderContent(forceShowList); + this.renderContent(forceShowList, hideNotEqual); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 762383f5a1d..055ed384011 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager { this.mapping[key].reference.setOffset(offset); } - load(key, firstLoad = false) { + load(key, firstLoad = false, dropdownKey = '') { const mappingKey = this.mapping[key]; const glClass = mappingKey.gl; const { element } = mappingKey; @@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager { } this.updateDropdownOffset(key); - mappingKey.reference.render(firstLoad, forceShowList); + mappingKey.reference.render(firstLoad, forceShowList, dropdownKey); this.currentDropdown = key; } - loadDropdown(dropdownName = '') { + loadDropdown(dropdownName = '', dropdownKey = '') { let firstLoad = false; if (!this.droplab) { @@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager { } if (dropdownName === DROPDOWN_TYPE.operator) { - this.load(dropdownName, firstLoad); + this.load(dropdownName, firstLoad, dropdownKey); return; } @@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { const key = match && match.key ? match.key : DROPDOWN_TYPE.hint; - this.load(key, firstLoad); + this.load(key, firstLoad, dropdownKey); } } @@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager { dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator; } - this.loadDropdown(dropdownToOpen); + this.loadDropdown(dropdownToOpen, dropdownName); } else if (lastToken) { const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator(); // Token has been initialized into an object because it has a value - this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator); + this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key); } else { this.loadDropdown(DROPDOWN_TYPE.hint); } diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index 200e5ba255f..5feac7485ad 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -1,17 +1,19 @@ <script> -/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; -import { GlButton } from '@gitlab/ui'; +import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import { s__, sprintf } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; export default { components: { - GlModal: DeprecatedModal2, + GlModal, GlButton, }, + directives: { + GlModalDirective, + SafeHtml, + }, props: { actionUrl: { type: String, @@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`), false, ); }, + primaryProps() { + return { + text: s__('Update username'), + attributes: [ + { variant: 'warning' }, + { category: 'primary' }, + { disabled: this.isRequestPending }, + ], + }; + }, + cancelProps() { + return { + text: s__('Cancel'), + }; + }, }, methods: { onConfirm() { @@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`), <p class="form-text text-muted">{{ path }}</p> </div> <gl-button - :data-target="`#${$options.modalId}`" + v-gl-modal-directive="$options.modalId" :disabled="isRequestPending || newUsername === username" category="primary" variant="warning" - data-toggle="modal" + data-testid="username-change-confirmation-modal" + >{{ $options.buttonText }}</gl-button > - {{ $options.buttonText }} - </gl-button> <gl-modal - :id="$options.modalId" - :header-title-text="s__('Profiles|Change username') + '?'" - :footer-primary-button-text="$options.buttonText" - footer-primary-button-variant="warning" - @submit="onConfirm" + :modal-id="$options.modalId" + :title="s__('Profiles|Change username') + '?'" + :action-primary="primaryProps" + :action-cancel="cancelProps" + @primary="onConfirm" > - <span v-html="modalText"></span> + <span v-safe-html="modalText"></span> </gl-modal> </div> </template> diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb new file mode 100644 index 00000000000..6665096d1a1 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerSetupResolver < BaseResolver + type Types::Ci::RunnerSetupType, null: true + + argument :platform, GraphQL::STRING_TYPE, + required: true, + description: 'Platform to generate the instructions for' + + argument :architecture, GraphQL::STRING_TYPE, + required: true, + description: 'Architecture to generate the instructions for' + + argument :project_id, ::Types::GlobalIDType[::Project], + required: false, + description: 'Project to register the runner for' + + argument :group_id, ::Types::GlobalIDType[::Group], + required: false, + description: 'Group to register the runner for' + + def resolve(platform:, architecture:, **args) + instructions = Gitlab::Ci::RunnerInstructions.new( + { current_user: current_user, os: platform, arch: architecture }.merge(target_param(args)) + ) + + { + install_instructions: instructions.install_script, + register_instructions: instructions.register_command + } + ensure + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError') + end + + private + + def target_param(args) + project_param(args[:project_id]) || group_param(args[:group_id]) || {} + end + + def project_param(project_id) + return unless project_id + + { project: find_object(project_id) } + end + + def group_param(group_id) + return unless group_id + + { group: find_object(group_id) } + end + + def find_object(gid) + GlobalID::Locator.locate(gid) + end + end + end +end diff --git a/app/graphql/types/ci/runner_setup_type.rb b/app/graphql/types/ci/runner_setup_type.rb new file mode 100644 index 00000000000..8bfc80f5dfe --- /dev/null +++ b/app/graphql/types/ci/runner_setup_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerSetupType < BaseObject + graphql_name 'RunnerSetup' + + field :install_instructions, GraphQL::STRING_TYPE, null: false, + description: 'Instructions for installing the runner on the specified architecture' + field :register_instructions, GraphQL::STRING_TYPE, null: false, + description: 'Instructions for registering the runner' + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index bd4b53bdaa7..a975035c305 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -84,6 +84,10 @@ module Types null: true, description: 'Supported runner platforms', resolver: Resolvers::Ci::RunnerPlatformsResolver + field :runner_setup, Types::Ci::RunnerSetupType, null: true, + description: 'Get runner setup instructions', + resolver: Resolvers::Ci::RunnerSetupResolver + def design_management DesignManagementObject.new(nil) end diff --git a/changelogs/unreleased/229674-update-change-username-modal.yml b/changelogs/unreleased/229674-update-change-username-modal.yml new file mode 100644 index 00000000000..ab2171c6918 --- /dev/null +++ b/changelogs/unreleased/229674-update-change-username-modal.yml @@ -0,0 +1,5 @@ +--- +title: Update change username modal +merge_request: 44325 +author: +type: changed diff --git a/changelogs/unreleased/mb_rails_save_bang_fix5.yml b/changelogs/unreleased/mb_rails_save_bang_fix5.yml new file mode 100644 index 00000000000..6c5cf6f09c5 --- /dev/null +++ b/changelogs/unreleased/mb_rails_save_bang_fix5.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang offenses for spec/services/* and spec/sidekiq/* +merge_request: 45391 +author: matthewbried +type: other diff --git a/changelogs/unreleased/ph-233486-removeDraftIsNotFilter.yml b/changelogs/unreleased/ph-233486-removeDraftIsNotFilter.yml new file mode 100644 index 00000000000..bba397eea82 --- /dev/null +++ b/changelogs/unreleased/ph-233486-removeDraftIsNotFilter.yml @@ -0,0 +1,5 @@ +--- +title: Removed not equal filter option for drafts on merge requests +merge_request: 45649 +author: +type: fixed diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 45377b4dd37..ad3afd6840b 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1695,7 +1695,7 @@ input BoardIssueInput { """ Filter by epic ID. Incompatible with epicWildcardId """ - epicId: ID + epicId: EpicID """ Filter by epic ID wildcard. Incompatible with epicId @@ -8572,6 +8572,11 @@ type Group { } """ +Identifier of Group +""" +scalar GroupID + +""" Represents a Group Membership """ type GroupMember implements MemberInterface { @@ -12568,7 +12573,7 @@ input NegatedBoardIssueInput { """ Filter by epic ID. Incompatible with epicWildcardId """ - epicId: ID + epicId: EpicID """ Filter by label name @@ -15630,6 +15635,31 @@ type Query { ): RunnerPlatformConnection """ + Get runner setup instructions + """ + runnerSetup( + """ + Architecture to generate the instructions for + """ + architecture: String! + + """ + Group to register the runner for + """ + groupId: GroupID + + """ + Platform to generate the instructions for + """ + platform: String! + + """ + Project to register the runner for + """ + projectId: ProjectID + ): RunnerSetup + + """ Find Snippets visible to the current user """ snippets( @@ -16922,6 +16952,18 @@ type RunnerPlatformEdge { node: RunnerPlatform } +type RunnerSetup { + """ + Instructions for installing the runner on the specified architecture + """ + installInstructions: String! + + """ + Instructions for registering the runner + """ + registerInstructions: String! +} + """ Represents a CI configuration of SAST """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 776fac99fab..f093d7e2dd3 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -4518,7 +4518,7 @@ "description": "Filter by epic ID. Incompatible with epicWildcardId", "type": { "kind": "SCALAR", - "name": "ID", + "name": "EpicID", "ofType": null }, "defaultValue": null @@ -23241,6 +23241,16 @@ "possibleTypes": null }, { + "kind": "SCALAR", + "name": "GroupID", + "description": "Identifier of Group", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "GroupMember", "description": "Represents a Group Membership", @@ -36941,7 +36951,7 @@ "description": "Filter by epic ID. Incompatible with epicWildcardId", "type": { "kind": "SCALAR", - "name": "ID", + "name": "EpicID", "ofType": null }, "defaultValue": null @@ -45235,6 +45245,67 @@ "deprecationReason": null }, { + "name": "runnerSetup", + "description": "Get runner setup instructions", + "args": [ + { + "name": "platform", + "description": "Platform to generate the instructions for", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "architecture", + "description": "Architecture to generate the instructions for", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "projectId", + "description": "Project to register the runner for", + "type": { + "kind": "SCALAR", + "name": "ProjectID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "groupId", + "description": "Group to register the runner for", + "type": { + "kind": "SCALAR", + "name": "GroupID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RunnerSetup", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "snippets", "description": "Find Snippets visible to the current user", "args": [ @@ -48856,6 +48927,55 @@ }, { "kind": "OBJECT", + "name": "RunnerSetup", + "description": null, + "fields": [ + { + "name": "installInstructions", + "description": "Instructions for installing the runner on the specified architecture", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "registerInstructions", + "description": "Instructions for registering the runner", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "SastCiConfiguration", "description": "Represents a CI configuration of SAST", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dca00fc1286..eeb72239172 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2276,6 +2276,13 @@ Autogenerated return type of RunDASTScan. | `humanReadableName` | String! | Human readable name of the runner platform | | `name` | String! | Name slug of the runner platform | +### RunnerSetup + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `installInstructions` | String! | Instructions for installing the runner on the specified architecture | +| `registerInstructions` | String! | Instructions for registering the runner | + ### SastCiConfigurationAnalyzersEntity Represents an analyzer entity in SAST CI configuration. diff --git a/doc/architecture/blueprints/image_resizing/index.md b/doc/architecture/blueprints/image_resizing/index.md new file mode 100644 index 00000000000..964e935cbb1 --- /dev/null +++ b/doc/architecture/blueprints/image_resizing/index.md @@ -0,0 +1,73 @@ +--- +comments: false +description: 'Image Resizing' +--- + +# Image resizing for avatars and content images + +Currently, we are showing all uploaded images 1:1, which is of course not ideal. To improve performance greatly we will add image resizing to the backend. There are two main areas of image resizing to consider; avatars and content images. The MVC for this implementation will focus on Avatars. Avatars requests consist of approximately 70% of total image requests. There is an identified set of sizes we intend to support which makes the scope of this first MVC very narrow. Content image resizing has many more considerations for size and features. It is entirely possible that we have two separate development efforts with the same goal of increasing performance via image resizing. + +## MVC Avatar Resizing + +We will implement a dynamic image resizing solution. This means image should be resized and optimized on the fly so that if we define new targeted sizes later we can add them dynamically. This would mean a huge improvement in performance as some of the measurements suggest that we can save up to 95% of our current load size. Our initial investigations indicate that we have uploaded approximately 1.65 million avatars totaling approximately 80GB in size and averaging approximately 48kb each. Early measurements indicate we can reduce the most common avatar dimensions to between 1-3kb in size, netting us a greater than 90% size reduction. For the MVC we will not consider application level caching and rely purely on HTTP based caches as implemented in CDNs and browsers, but might revisit this decision later on. To easily mitigate performance issues with avatar resizing, especially in the case of self managed, an operations feature flag will be implemented to disable dynamic image resizing. + +```mermaid +sequenceDiagram + autonumber + Requester->>Workhorse: Incoming request + Workhorse->>RailsApp: Incoming request + alt All is true: 1.Avatar is requested, 2.Requested Width is allowed, 3.Feature is enabled + Note right of RailsApp: Width Allowlist: https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/concerns/avatarable.rb#L10 + RailsApp->>Workhorse: `send-scaled-img:` request + Note right of RailsApp: Set `send-scaled-img:` Header + Workhorse->>Workhorse: Image resizing using Go lib + Workhorse->>Requester: Serve the resized image + else All other cases + RailsApp->>Workhorse: Usual request scenario + Workhorse->>Requester: Usual request scenario + end +``` + +## Content Image Resizing + +Content image resizing is a more complex problem to tackle. There are no set size restrictions and there are additional features or requirements to consider. + +- Dynamic WebP support - the WebP format typically achieves an average of 30% more compression than JPEG without the loss of image quality. More details [here](https://developers.google.com/speed/webp/docs/c_study) +- Extract first image of GIF's so we can prevent from loading 10MB pixels +- Check Device Pixel Ratio to deliver nice images on High DPI screens +- Progressive image loading, similar to what is described [here](https://www.sitepoint.com/how-to-build-your-own-progressive-image-loader/) +- Resizing recommendations (size, clarity, etc.) +- Storage + +The MVC Avatar resizing implementation is integrated into Workhorse. With the extra requirements for content image resizing, this may require further use of GraphicsMagik (GM) or a similar library and breaking it out of Workhorse. + +## Iterations + +1. [x] POC on different image resizing solutions +1. [x] Review solutions with security team +1. [x] Implement avatar resizing MVC +1. [ ] Deploy, measure, monitor +1. [ ] Clarify features for content image resizing +1. [ ] Weigh options between using current implementation of image resizing vs new solution +1. [ ] Implement content image resizing MVC +1. [ ] Deploy, measure, monitor + +## Who + +Proposal: + +| Role | Who +|------------------------------|-------------------------| +| Author | Craig Gomes | +| Architecture Evolution Coach | Kamil Trzciński | +| Engineering Leader | Tim Zallmann | +| Domain Expert | Matthias Kaeppler | +| Domain Expert | Aleksei Lipniagov | + +DRIs: + +| Role | Who +|------------------------------|------------------------| +| Product | Josh Lambert | +| Leadership | Craig Gomes | +| Engineering | Matthias Kaeppler | diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index 70bd3215ec5..2e1556f09db 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -219,10 +219,17 @@ You can also specify a static part of the URL at `environment:url:`, such as The assigned URL for the `review/your-branch-name` environment is [visible in the UI](#using-the-environment-url). -> **Notes:** -> -> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job. -> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL. +Note the following: + +- `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the + `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you shouldn't set `environment:url:` in the + `stop_review` job. +- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update + the environment URL. +- If the script that runs in `stop_review` exists only in your repository and therefore can't use + `GIT_STRATEGY: none`, configure [pipelines for merge requests](../../ci/merge_request_pipelines/index.md) + for these jobs. This ensures that runners can fetch the repository even after a feature branch is + deleted. For more information, see [Ref Specs for Runners](../pipelines/index.md#ref-specs-for-runners). ### Configuring manual deployments @@ -675,24 +682,23 @@ deploy_review: name: review/$CI_COMMIT_REF_NAME url: https://$CI_ENVIRONMENT_SLUG.example.com on_stop: stop_review - only: - - branches - except: - - master + rules: + - if: $CI_MERGE_REQUEST_ID stop_review: stage: deploy - variables: - GIT_STRATEGY: none script: - echo "Remove review app" - when: manual environment: name: review/$CI_COMMIT_REF_NAME action: stop + rules: + - if: $CI_MERGE_REQUEST_ID + when: manual ``` -Setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the +If you can't use [Pipelines for merge requests](../merge_request_pipelines/index.md), +setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the `stop_review` job so that the [runner](https://docs.gitlab.com/runner/) won't try to check out the code after the branch is deleted. @@ -748,13 +754,17 @@ review_app: name: review/$CI_COMMIT_REF_NAME on_stop: stop_review_app auto_stop_in: 1 week + rules: + - if: $CI_MERGE_REQUEST_ID stop_review_app: script: stop-review-app environment: name: review/$CI_COMMIT_REF_NAME action: stop - when: manual + rules: + - if: $CI_MERGE_REQUEST_ID + when: manual ``` As long as a merge request is active and keeps getting new commits, diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 9aeedc7d448..cc393a4c44e 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -79,6 +79,27 @@ You can also configure specific aspects of your pipelines through the GitLab UI. - [Pipeline schedules](schedules.md). - [Custom CI/CD variables](../variables/README.md#custom-environment-variables). +### Ref Specs for Runners + +When a runner picks a pipeline job, GitLab provides that job's metadata. This includes the [Git refspecs](https://git-scm.com/book/en/v2/Git-Internals-The-Refspec), +which indicate which ref (branch, tag, and so on) and commit (SHA1) are checked out from your +project repository. + +This table lists the refspecs injected for each pipeline type: + +| Pipeline type | Refspecs | +|--------------- |---------------------------------------- | +| Pipeline for Branches | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/heads/<name>:refs/remotes/origin/<name>` | +| pipeline for Tags | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/tags/<name>:refs/tags/<name>` | +| [Pipeline for Merge Requests](../merge_request_pipelines/index.md) | `+refs/pipelines/<id>:refs/pipelines/<id>` | + +The refs `refs/heads/<name>` and `refs/tags/<name>` exist in your +project repository. GitLab generates the special ref `refs/pipelines/<id>` during a +running pipeline job. This ref can be created even after the associated branch or tag has been +deleted. It's therefore useful in some features such as [automatically stopping an environment](../environments/index.md#automatically-stopping-an-environment), +and [merge trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md) +that might run pipelines after branch deletion. + ### View pipelines You can find the current and historical pipeline runs under your project's diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index fffaf4ad26b..1e83d7b8bf0 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -489,15 +489,15 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia | `DAST_API_HOST_OVERRIDE` | string | Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080` | | `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. | | `DAST_REQUEST_HEADERS` | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` | -| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false` | -| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. | -| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. | -| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. | -| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. | -| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false` | -| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false` | -| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html` | -| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. | +| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1.| +| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | +| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214120) in GitLab 13.4. | +| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. | | `DAST_ZAP_LOG_CONFIGURATION` | string | Set to a semicolon-separated list of additional log4j properties for the ZAP Server. For example, `log4j.logger.org.parosproxy.paros.network.HttpSender=DEBUG;log4j.logger.com.crawljax=DEBUG` | ### DAST command-line options diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md index 6167c0445f9..b6596a93939 100644 --- a/doc/user/application_security/sast/analyzers.md +++ b/doc/user/application_security/sast/analyzers.md @@ -33,7 +33,7 @@ SAST supports the following official analyzers: - [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix)) - [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT)) -The analyzers are published as Docker images that SAST will use to launch +The analyzers are published as Docker images that SAST uses to launch dedicated containers for each analysis. SAST is pre-configured with a set of **default images** that are maintained by @@ -77,12 +77,12 @@ variables: SAST_DEFAULT_ANALYZERS: "bandit,flawfinder" ``` -`bandit` runs first. When merging the reports, SAST will -remove the duplicates and will keep the `bandit` entries. +`bandit` runs first. When merging the reports, SAST +removes the duplicates and keeps the `bandit` entries. ### Disabling default analyzers -Setting `SAST_DEFAULT_ANALYZERS` to an empty string will disable all the official +Setting `SAST_DEFAULT_ANALYZERS` to an empty string disables all the official default analyzers. In `.gitlab-ci.yml` define: ```yaml diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 76f9197cd95..6be71f540ad 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -189,7 +189,7 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. > - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4 and later. -> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5. +> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6. To help you track the status of your issues, you can assign a status to each issue to flag work that's progressing as planned or needs attention to keep on schedule: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 67c638a6827..830d5545c04 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28386,6 +28386,9 @@ msgstr "" msgid "Update now" msgstr "" +msgid "Update username" +msgstr "" + msgid "Update variable" msgstr "" diff --git a/spec/features/merge_requests/user_filters_by_draft_spec.rb b/spec/features/merge_requests/user_filters_by_draft_spec.rb new file mode 100644 index 00000000000..de070805d96 --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_draft_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge Requests > User filters by draft', :js do + include FilteredSearchHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + + before do + create(:merge_request, title: 'Draft: Bugfix', source_project: project, target_project: project, source_branch: 'bugfix2') + + sign_in(user) + visit project_merge_requests_path(project) + end + + it 'filters results' do + input_filtered_search_keys('draft:=yes') + + expect(page).to have_content('Draft: Bugfix') + end + + it 'does not allow filtering by is not equal' do + find('#filtered-search-merge_requests').click + + click_button 'Draft' + + expect(page).not_to have_content('!=') + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 4326700bab1..5d3a00017f2 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -101,10 +101,10 @@ RSpec.describe 'Profile account page', :js do it 'changes my username' do fill_in 'username-change-input', with: 'new-username' - page.find('[data-target="#username-change-confirmation-modal"]').click + page.find('[data-testid="username-change-confirmation-modal"]').click page.within('.modal') do - find('.js-modal-primary-action').click + find('.js-modal-action-primary').click end expect(page).to have_content('new-username') diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 13ec12c50ea..62d8a96c1b2 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -128,10 +128,10 @@ def update_username(new_username) fill_in 'username-change-input', with: new_username - page.find('[data-target="#username-change-confirmation-modal"]').click + page.find('[data-testid="username-change-confirmation-modal"]').click page.within('.modal') do - find('.js-modal-primary-action').click + find('.js-modal-action-primary').click end wait_for_requests diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js index be39a7f4d80..45e5e0f885f 100644 --- a/spec/frontend/profile/account/components/update_username_spec.js +++ b/spec/frontend/profile/account/components/update_username_spec.js @@ -1,173 +1,135 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; -import mountComponent from 'helpers/vue_mount_component_helper'; import axios from '~/lib/utils/axios_utils'; -import updateUsername from '~/profile/account/components/update_username.vue'; +import UpdateUsername from '~/profile/account/components/update_username.vue'; describe('UpdateUsername component', () => { const rootUrl = TEST_HOST; const actionUrl = `${TEST_HOST}/update/username`; - const username = 'hasnoname'; - const newUsername = 'new_username'; - let Component; - let vm; + const defaultProps = { + actionUrl, + rootUrl, + initialUsername: 'hasnoname', + }; + let wrapper; let axiosMock; + const createComponent = (props = {}) => { + wrapper = shallowMount(UpdateUsername, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal, + }, + }); + }; + beforeEach(() => { axiosMock = new MockAdapter(axios); - Component = Vue.extend(updateUsername); - vm = mountComponent(Component, { - actionUrl, - rootUrl, - initialUsername: username, - }); + createComponent(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); axiosMock.restore(); }); const findElements = () => { - const modalSelector = `#${vm.$options.modalId}`; + const modal = wrapper.find(GlModal); return { - input: vm.$el.querySelector(`#${vm.$options.inputId}`), - openModalBtn: vm.$el.querySelector(`[data-target="${modalSelector}"]`), - modal: vm.$el.querySelector(modalSelector), - modalBody: vm.$el.querySelector(`${modalSelector} .modal-body`), - modalHeader: vm.$el.querySelector(`${modalSelector} .modal-title`), - confirmModalBtn: vm.$el.querySelector(`${modalSelector} .btn-warning`), + modal, + input: wrapper.find(`#${wrapper.vm.$options.inputId}`), + openModalBtn: wrapper.find('[data-testid="username-change-confirmation-modal"]'), + modalBody: modal.find('.modal-body'), + modalHeader: modal.find('.modal-title'), + confirmModalBtn: wrapper.find('.btn-warning'), }; }; - it('has a disabled button if the username was not changed', done => { - const { input, openModalBtn } = findElements(); - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.username).toBe(username); - expect(vm.newUsername).toBe(username); - expect(openModalBtn).toBeDisabled(); - }) - .then(done) - .catch(done.fail); + it('has a disabled button if the username was not changed', async () => { + const { openModalBtn } = findElements(); + + await wrapper.vm.$nextTick(); + + expect(openModalBtn.props('disabled')).toBe(true); }); - it('has an enabled button which if the username was changed', done => { + it('has an enabled button which if the username was changed', async () => { const { input, openModalBtn } = findElements(); - input.value = newUsername; - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.username).toBe(username); - expect(vm.newUsername).toBe(newUsername); - expect(openModalBtn).not.toBeDisabled(); - }) - .then(done) - .catch(done.fail); - }); - it('confirmation modal contains proper header and body', done => { - const { modalBody, modalHeader } = findElements(); + input.element.value = 'newUsername'; + input.trigger('input'); - vm.newUsername = newUsername; + await wrapper.vm.$nextTick(); - Vue.nextTick() - .then(() => { - expect(modalHeader.textContent).toContain('Change username?'); - expect(modalBody.textContent).toContain( - `You are going to change the username ${username} to ${newUsername}`, - ); - }) - .then(done) - .catch(done.fail); + expect(openModalBtn.props('disabled')).toBe(false); }); - it('confirmation modal should escape usernames properly', done => { - const { modalBody } = findElements(); + describe('changing username', () => { + const newUsername = 'new_username'; - vm.username = '<i>Italic</i>'; - vm.newUsername = vm.username; + beforeEach(async () => { + createComponent(); + wrapper.setData({ newUsername }); - Vue.nextTick() - .then(() => { - expect(modalBody.innerHTML).toContain('<i>Italic</i>'); - expect(modalBody.innerHTML).not.toContain(vm.username); - }) - .then(done) - .catch(done.fail); - }); + await wrapper.vm.$nextTick(); + }); - it('executes API call on confirmation button click', done => { - const { confirmModalBtn } = findElements(); + it('confirmation modal contains proper header and body', async () => { + const { modal } = findElements(); - axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]); - jest.spyOn(axios, 'put'); + expect(modal.attributes('title')).toBe('Change username?'); + expect(modal.text()).toContain( + `You are going to change the username ${defaultProps.initialUsername} to ${newUsername}`, + ); + }); - vm.newUsername = newUsername; + it('executes API call on confirmation button click', async () => { + axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]); + jest.spyOn(axios, 'put'); - Vue.nextTick() - .then(() => { - confirmModalBtn.click(); + await wrapper.vm.onConfirm(); + await wrapper.vm.$nextTick(); - expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } }); - }) - .then(done) - .catch(done.fail); - }); + expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } }); + }); - it('sets the username after a successful update', done => { - const { input, openModalBtn } = findElements(); + it('sets the username after a successful update', async () => { + const { input, openModalBtn } = findElements(); - axiosMock.onPut(actionUrl).replyOnce(() => { - expect(input).toBeDisabled(); - expect(openModalBtn).toBeDisabled(); + axiosMock.onPut(actionUrl).replyOnce(() => { + expect(input.attributes('disabled')).toBe('disabled'); + expect(openModalBtn.props('disabled')).toBe(true); - return [200, { message: 'Username changed' }]; + return [200, { message: 'Username changed' }]; + }); + + await wrapper.vm.onConfirm(); + await wrapper.vm.$nextTick(); + + expect(input.attributes('disabled')).toBe(undefined); + expect(openModalBtn.props('disabled')).toBe(true); }); - vm.newUsername = newUsername; - - vm.onConfirm() - .then(() => { - expect(vm.username).toBe(newUsername); - expect(vm.newUsername).toBe(newUsername); - expect(input).not.toBeDisabled(); - expect(input.value).toBe(newUsername); - expect(openModalBtn).toBeDisabled(); - }) - .then(done) - .catch(done.fail); - }); + it('does not set the username after a erroneous update', async () => { + const { input, openModalBtn } = findElements(); - it('does not set the username after a erroneous update', done => { - const { input, openModalBtn } = findElements(); + axiosMock.onPut(actionUrl).replyOnce(() => { + expect(input.attributes('disabled')).toBe('disabled'); + expect(openModalBtn.props('disabled')).toBe(true); - axiosMock.onPut(actionUrl).replyOnce(() => { - expect(input).toBeDisabled(); - expect(openModalBtn).toBeDisabled(); + return [400, { message: 'Invalid username' }]; + }); - return [400, { message: 'Invalid username' }]; + await expect(wrapper.vm.onConfirm()).rejects.toThrow(); + expect(input.attributes('disabled')).toBe(undefined); + expect(openModalBtn.props('disabled')).toBe(false); }); - - const invalidUsername = 'anything.git'; - vm.newUsername = invalidUsername; - - vm.onConfirm() - .then(() => done.fail('Expected onConfirm to throw!')) - .catch(() => { - expect(vm.username).toBe(username); - expect(vm.newUsername).toBe(invalidUsername); - expect(input).not.toBeDisabled(); - expect(input.value).toBe(invalidUsername); - expect(openModalBtn).not.toBeDisabled(); - }) - .then(done) - .catch(done.fail); }); }); diff --git a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb new file mode 100644 index 00000000000..e6f76c8cd2d --- /dev/null +++ b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::RunnerSetupResolver do + include GraphqlHelpers + + describe '#resolve' do + let(:user) { create(:user) } + + subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: 'linux', architecture: 'amd64' }.merge(target_param)) } + + context 'without target parameter' do + let(:target_param) { {} } + + context 'when user is not admin' do + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is admin' do + before do + user.update!(admin: true) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + end + + context 'with project target parameter' do + let(:project) { create(:project) } + let(:target_param) { { project_id: project.to_global_id } } + + context 'when user has access to admin builds on project' do + before do + project.add_maintainer(user) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + + context 'when user does not have access to admin builds on project' do + before do + project.add_developer(user) + end + + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'with group target parameter' do + let(:group) { create(:group) } + let(:target_param) { { group_id: group.to_global_id } } + + context 'when user has access to admin builds on group' do + before do + group.add_owner(user) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + + context 'when user does not have access to admin builds on group' do + before do + group.add_developer(user) + end + + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + end +end diff --git a/spec/graphql/types/ci/runner_setup_type_spec.rb b/spec/graphql/types/ci/runner_setup_type_spec.rb new file mode 100644 index 00000000000..197e717e964 --- /dev/null +++ b/spec/graphql/types/ci/runner_setup_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RunnerSetupType do + specify { expect(described_class.graphql_name).to eq('RunnerSetup') } + + it 'exposes the expected fields' do + expected_fields = %i[ + install_instructions + register_instructions + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 1d9ca8323f8..eee92fbb61d 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -80,4 +80,12 @@ RSpec.describe GitlabSchema.types['Query'] do is_expected.to have_graphql_type(Types::Ci::RunnerPlatformType.connection_type) end end + + describe 'runner_setup field' do + subject { described_class.fields['runnerSetup'] } + + it 'returns runner setup instructions' do + is_expected.to have_graphql_type(Types::Ci::RunnerSetupType) + end + end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index f78cad74c5e..41c96c0989b 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -642,7 +642,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do let_it_be(:project) { create(:project, :public, container_registry_enabled: false) } before do - project.update(container_registry_enabled: false) + project.update!(container_registry_enabled: false) end context 'disallow when pulling' do diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 98fa6012089..1d33dc15838 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -131,7 +131,7 @@ RSpec.describe AutoMerge::BaseService do end describe '#update' do - subject { service.update(merge_request) } + subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index eab95973e1b..3f7a26aa00e 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -148,7 +148,7 @@ RSpec.describe AutoMergeService do end describe '#update' do - subject { service.update(merge_request) } + subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang context 'when auto merge is enabled' do let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb index e496ccd5c23..9aead97f41c 100644 --- a/spec/services/clusters/update_service_spec.rb +++ b/spec/services/clusters/update_service_spec.rb @@ -197,7 +197,7 @@ RSpec.describe Clusters::UpdateService do context 'manangement_project is outside of the namespace scope' do before do - management_project.update(group: create(:group)) + management_project.update!(group: create(:group)) end let(:params) do @@ -224,7 +224,7 @@ RSpec.describe Clusters::UpdateService do context 'manangement_project is outside of the namespace scope' do before do - management_project.update(group: create(:group)) + management_project.update!(group: create(:group)) end let(:params) do diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb index 749030af97d..e06b6fbf116 100644 --- a/spec/services/design_management/generate_image_versions_service_spec.rb +++ b/spec/services/design_management/generate_image_versions_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do end it 'logs if the raw image cannot be found' do - version.designs.first.update(filename: 'foo.png') + version.designs.first.update!(filename: 'foo.png') expect(Gitlab::AppLogger).to receive(:error).with("No design file found for Action: #{action.id}") diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb index 5ff0d535b46..42c4ef52741 100644 --- a/spec/services/discussions/resolve_service_spec.rb +++ b/spec/services/discussions/resolve_service_spec.rb @@ -40,11 +40,11 @@ RSpec.describe Discussions::ResolveService do context 'with a project that requires all discussion to be resolved' do before do - project.update(only_allow_merge_if_all_discussions_are_resolved: true) + project.update!(only_allow_merge_if_all_discussions_are_resolved: true) end after do - project.update(only_allow_merge_if_all_discussions_are_resolved: false) + project.update!(only_allow_merge_if_all_discussions_are_resolved: false) end let_it_be(:other_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion } diff --git a/spec/services/draft_notes/destroy_service_spec.rb b/spec/services/draft_notes/destroy_service_spec.rb index f725f08f3c7..1f246a56eb3 100644 --- a/spec/services/draft_notes/destroy_service_spec.rb +++ b/spec/services/draft_notes/destroy_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe DraftNotes::DestroyService do it 'destroys all draft notes for a user in a merge request' do create_list(:draft_note, 2, merge_request: merge_request, author: user) - expect { destroy }.to change { DraftNote.count }.by(-2) + expect { destroy }.to change { DraftNote.count }.by(-2) # rubocop:disable Rails/SaveBang expect(DraftNote.count).to eq(0) end @@ -45,7 +45,7 @@ RSpec.describe DraftNotes::DestroyService do allow_any_instance_of(DraftNote).to receive_message_chain(:diff_file, :unfolded?) { true } expect(merge_request).to receive_message_chain(:diffs, :clear_cache) - destroy + destroy # rubocop:disable Rails/SaveBang end end end diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb index 935a673f548..d3a745bc744 100644 --- a/spec/services/emails/confirm_service_spec.rb +++ b/spec/services/emails/confirm_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Emails::ConfirmService do describe '#execute' do it 'enqueues a background job to send confirmation email again' do - email = user.emails.create(email: 'new@email.com') + email = user.emails.create!(email: 'new@email.com') expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers') end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 31afdba8192..e06f09d0463 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -95,7 +95,7 @@ RSpec.describe Groups::DestroyService do context 'projects in pending_delete' do before do project.pending_delete = true - project.save + project.save! end it_behaves_like 'group destruction', false diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb index f284225e23a..f8cb55a9955 100644 --- a/spec/services/groups/import_export/import_service_spec.rb +++ b/spec/services/groups/import_export/import_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe Groups::ImportExport::ImportService do before do stub_feature_flags(group_import_ndjson: false) - ImportExportUpload.create(group: group, import_file: import_file) + ImportExportUpload.create!(group: group, import_file: import_file) allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger) allow(import_logger).to receive(:error) @@ -105,7 +105,7 @@ RSpec.describe Groups::ImportExport::ImportService do subject { service.execute } before do - ImportExportUpload.create(group: group, import_file: import_file) + ImportExportUpload.create!(group: group, import_file: import_file) allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger) allow(import_logger).to receive(:error) @@ -216,7 +216,7 @@ RSpec.describe Groups::ImportExport::ImportService do subject { service.execute } before do - ImportExportUpload.create(group: group, import_file: import_file) + ImportExportUpload.create!(group: group, import_file: import_file) allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger) allow(import_logger).to receive(:error) diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index 7674ec36331..cc4be6720cc 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -89,10 +89,10 @@ RSpec.describe Labels::PromoteService do it 'keeps users\' subscriptions' do user2 = create(:user) - project_label_1_1.subscriptions.create(user: user, subscribed: true) - project_label_2_1.subscriptions.create(user: user, subscribed: true) - project_label_3_2.subscriptions.create(user: user, subscribed: true) - project_label_2_1.subscriptions.create(user: user2, subscribed: true) + project_label_1_1.subscriptions.create!(user: user, subscribed: true) + project_label_2_1.subscriptions.create!(user: user, subscribed: true) + project_label_3_2.subscriptions.create!(user: user, subscribed: true) + project_label_2_1.subscriptions.create!(user: user2, subscribed: true) expect { service.execute(project_label_1_1) }.to change { Subscription.count }.from(4).to(3) diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 1e5536a2d0b..70b5e70cab6 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -286,7 +286,7 @@ RSpec.describe Notes::CreateService do QuickAction.new( action_text: "/wip", before_action: -> { - issuable.reload.update(title: "title") + issuable.reload.update!(title: "title") }, expectation: ->(issuable, can_use_quick_action) { expect(issuable.work_in_progress?).to eq(can_use_quick_action) @@ -296,7 +296,7 @@ RSpec.describe Notes::CreateService do QuickAction.new( action_text: "/wip", before_action: -> { - issuable.reload.update(title: "WIP: title") + issuable.reload.update!(title: "WIP: title") }, expectation: ->(noteable, can_use_quick_action) { expect(noteable.work_in_progress?).not_to eq(can_use_quick_action) diff --git a/spec/services/notification_recipients/build_service_spec.rb b/spec/services/notification_recipients/build_service_spec.rb index 5c8add250c2..cc08f9fceff 100644 --- a/spec/services/notification_recipients/build_service_spec.rb +++ b/spec/services/notification_recipients/build_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe NotificationRecipients::BuildService do context 'when there are multiple subscribers' do def create_user subscriber = create(:user) - issue.subscriptions.create(user: subscriber, project: project, subscribed: true) + issue.subscriptions.create!(user: subscriber, project: project, subscribed: true) end include_examples 'no N+1 queries' @@ -96,7 +96,7 @@ RSpec.describe NotificationRecipients::BuildService do context 'when there are multiple subscribers' do def create_user subscriber = create(:user) - merge_request.subscriptions.create(user: subscriber, project: project, subscribed: true) + merge_request.subscriptions.create!(user: subscriber, project: project, subscribed: true) end include_examples 'no N+1 queries' diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index caa9961424e..99f52e01031 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -361,7 +361,7 @@ RSpec.describe NotificationService, :mailer do context 'where the project has disabled the feature' do before do - project.update(service_desk_enabled: false) + project.update!(service_desk_enabled: false) end it_should_not_email! @@ -453,7 +453,7 @@ RSpec.describe NotificationService, :mailer do context 'by note' do before do note.author = @u_lazy_participant - note.save + note.save! end it { expect { subject }.not_to have_enqueued_email(@u_lazy_participant.id, note.id, mail: "note_issue_email") } @@ -467,7 +467,7 @@ RSpec.describe NotificationService, :mailer do note.project.namespace_id = group.id group.add_user(@u_watcher, GroupMember::MAINTAINER) group.add_user(@u_custom_global, GroupMember::MAINTAINER) - note.project.save + note.project.save! @u_watcher.notification_settings_for(note.project).participating! @u_watcher.notification_settings_for(group).global! @@ -1719,7 +1719,7 @@ RSpec.describe NotificationService, :mailer do before do merge_request.author = participant - merge_request.save + merge_request.save! notification.new_merge_request(merge_request, @u_disabled) end @@ -1962,7 +1962,7 @@ RSpec.describe NotificationService, :mailer do describe 'when merge_when_pipeline_succeeds is true' do before do - merge_request.update( + merge_request.update!( merge_when_pipeline_succeeds: true, merge_user: create(:user) ) @@ -2725,7 +2725,7 @@ RSpec.describe NotificationService, :mailer do before do group = create(:group) - project.update(group: group) + project.update!(group: group) create(:email, :confirmed, user: u_custom_notification_enabled, email: group_notification_email) create(:notification_setting, user: u_custom_notification_enabled, source: group, notification_email: group_notification_email) @@ -2761,7 +2761,7 @@ RSpec.describe NotificationService, :mailer do before do group = create(:group) - project.update(group: group) + project.update!(group: group) create(:email, :confirmed, user: u_member, email: group_notification_email) create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email) end @@ -2821,7 +2821,7 @@ RSpec.describe NotificationService, :mailer do context 'when the creator has no read_build access' do before do pipeline = create_pipeline(u_member, :failed) - project.update(public_builds: false) + project.update!(public_builds: false) project.team.truncate notification.pipeline_finished(pipeline) end @@ -2855,7 +2855,7 @@ RSpec.describe NotificationService, :mailer do before do group = create(:group) - project.update(group: group) + project.update!(group: group) create(:email, :confirmed, user: u_member, email: group_notification_email) create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email) end @@ -3212,7 +3212,7 @@ RSpec.describe NotificationService, :mailer do # with different notification settings def build_group(project, visibility: :public) group = create_nested_group(visibility) - project.update(namespace_id: group.id) + project.update!(namespace_id: group.id) # Group member: global=disabled, group=watch @g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group) @@ -3277,11 +3277,11 @@ RSpec.describe NotificationService, :mailer do end def add_user_subscriptions(issuable) - issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false) - issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true) - issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true) - issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false) + issuable.subscriptions.create!(user: @unsubscribed_mentioned, project: project, subscribed: false) + issuable.subscriptions.create!(user: @subscriber, project: project, subscribed: true) + issuable.subscriptions.create!(user: @subscribed_participant, project: project, subscribed: true) + issuable.subscriptions.create!(user: @unsubscriber, project: project, subscribed: false) # Make the watcher a subscriber to detect dupes - issuable.subscriptions.create(user: @watcher_and_subscriber, project: project, subscribed: true) + issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true) end end diff --git a/spec/services/packages/conan/create_package_file_service_spec.rb b/spec/services/packages/conan/create_package_file_service_spec.rb index 0e9cbba5fc1..bd6a91c883a 100644 --- a/spec/services/packages/conan/create_package_file_service_spec.rb +++ b/spec/services/packages/conan/create_package_file_service_spec.rb @@ -100,7 +100,7 @@ RSpec.describe Packages::Conan::CreatePackageFileService do end let(:tmp_object) do - fog_connection.directories.new(key: 'packages').files.create( + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang key: "tmp/uploads/#{file_name}", body: 'content' ) diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb index 3e79270da8d..165b38ee338 100644 --- a/spec/services/reset_project_cache_service_spec.rb +++ b/spec/services/reset_project_cache_service_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ResetProjectCacheService do context 'when project cache_index is a numeric value' do before do - project.update(jobs_cache_index: 1) + project.update!(jobs_cache_index: 1) end it 'increments project cache index' do diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb index 3a9dadbd40e..a2131c5c1b0 100644 --- a/spec/services/resource_events/change_milestone_service_spec.rb +++ b/spec/services/resource_events/change_milestone_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do [:issue, :merge_request].each do |issuable| it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do - let_it_be(:resource) { create(issuable) } + let_it_be(:resource) { create(issuable) } # rubocop:disable Rails/SaveBang end end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index bdc40a92e91..b25837ca260 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -85,7 +85,7 @@ RSpec.describe SystemHooksService do end it 'handles nil datetime columns' do - user.update(created_at: nil, updated_at: nil) + user.update!(created_at: nil, updated_at: nil) data = event_data(user, :destroy) expect(data[:created_at]).to be(nil) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 42e48b9ad81..abb0b79de84 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -378,13 +378,13 @@ RSpec.describe SystemNoteService do noteable_types.each do |type| context "when noteable is a #{type}" do it "blocks cross reference when #{type.underscore}_events is false" do - jira_tracker.update("#{type}_events" => false) + jira_tracker.update!("#{type}_events" => false) expect(cross_reference(type)).to eq(s_('JiraService|Events for %{noteable_model_name} are disabled.') % { noteable_model_name: type.pluralize.humanize.downcase }) end it "creates cross reference when #{type.underscore}_events is true" do - jira_tracker.update("#{type}_events" => true) + jira_tracker.update!("#{type}_events" => true) expect(cross_reference(type)).to eq(success_message) end diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb index e78b00fb67a..b70c5e899fc 100644 --- a/spec/services/system_notes/issuables_service_spec.rb +++ b/spec/services/system_notes/issuables_service_spec.rb @@ -373,7 +373,7 @@ RSpec.describe ::SystemNotes::IssuablesService do before do # Mention issue (noteable) from commit0 system_note = service.cross_reference(commit0) - system_note.update(note: system_note.note.capitalize) + system_note.update!(note: system_note.note.capitalize) end it 'is truthy when already mentioned' do @@ -407,7 +407,7 @@ RSpec.describe ::SystemNotes::IssuablesService do before do # Mention commit1 from commit0 system_note = service.cross_reference(commit1) - system_note.update(note: system_note.note.capitalize) + system_note.update!(note: system_note.note.capitalize) end it 'is truthy when already mentioned' do @@ -436,7 +436,7 @@ RSpec.describe ::SystemNotes::IssuablesService do context 'legacy capitalized cross reference' do before do system_note = service.cross_reference(commit0) - system_note.update(note: system_note.note.capitalize) + system_note.update!(note: system_note.note.capitalize) end it 'is true when a fork mentions an external issue' do @@ -582,7 +582,7 @@ RSpec.describe ::SystemNotes::IssuablesService do it 'creates the note text correctly' do [:issue, :merge_request].each do |type| - issuable = create(type) + issuable = create(type) # rubocop:disable Rails/SaveBang service = described_class.new(noteable: issuable, author: author) expect(service.discussion_lock.note) diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 60903f8f367..748003a26d1 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -145,7 +145,7 @@ RSpec.describe TodoService do end it 'creates correct todos for each valid user based on the type of mention' do - issue.update(description: directly_addressed_and_mentioned) + issue.update!(description: directly_addressed_and_mentioned) service.new_issue(issue, author) @@ -222,7 +222,7 @@ RSpec.describe TodoService do end it 'creates a todo for each valid user not included in skip_users based on the type of mention' do - issue.update(description: directly_addressed_and_mentioned) + issue.update!(description: directly_addressed_and_mentioned) service.update_issue(issue, author, skip_users) @@ -291,7 +291,7 @@ RSpec.describe TodoService do context 'issues with a task list' do it 'does not create todo when tasks are marked as completed' do - issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + issue.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") service.update_issue(issue, author) @@ -304,7 +304,7 @@ RSpec.describe TodoService do end it 'does not create directly addressed todo when tasks are marked as completed' do - addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n") + addressed_issue.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n") service.update_issue(addressed_issue, author) @@ -317,7 +317,7 @@ RSpec.describe TodoService do end it 'does not raise an error when description not change' do - issue.update(title: 'Sample') + issue.update!(title: 'Sample') expect { service.update_issue(issue, author) }.not_to raise_error end @@ -427,7 +427,7 @@ RSpec.describe TodoService do end it 'creates a todo for each valid user based on the type of mention' do - note.update(note: directly_addressed_and_mentioned) + note.update!(note: directly_addressed_and_mentioned) service.new_note(note, john_doe) @@ -694,7 +694,7 @@ RSpec.describe TodoService do end it 'creates a todo for each valid user based on the type of mention' do - mr_assigned.update(description: directly_addressed_and_mentioned) + mr_assigned.update!(description: directly_addressed_and_mentioned) service.new_merge_request(mr_assigned, author) @@ -726,7 +726,7 @@ RSpec.describe TodoService do end it 'creates a todo for each valid user not included in skip_users based on the type of mention' do - mr_assigned.update(description: directly_addressed_and_mentioned) + mr_assigned.update!(description: directly_addressed_and_mentioned) service.update_merge_request(mr_assigned, author, skip_users) @@ -772,7 +772,7 @@ RSpec.describe TodoService do context 'with a task list' do it 'does not create todo when tasks are marked as completed' do - mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + mr_assigned.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") service.update_merge_request(mr_assigned, author) @@ -786,7 +786,7 @@ RSpec.describe TodoService do end it 'does not create directly addressed todo when tasks are marked as completed' do - addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2") + addressed_mr_assigned.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2") service.update_merge_request(addressed_mr_assigned, author) @@ -800,7 +800,7 @@ RSpec.describe TodoService do end it 'does not raise an error when description not change' do - mr_assigned.update(title: 'Sample') + mr_assigned.update!(title: 'Sample') expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error end @@ -883,7 +883,7 @@ RSpec.describe TodoService do end it 'creates a pending todo for each merge_participant' do - mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) + mr_unassigned.update!(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_became_unmergeable(mr_unassigned) merge_participants.each do |participant| @@ -991,7 +991,7 @@ RSpec.describe TodoService do end it 'creates a todo for each valid user not included in skip_users based on the type of mention' do - note.update(note: directly_addressed_and_mentioned) + note.update!(note: directly_addressed_and_mentioned) service.update_note(note, author, skip_users) diff --git a/spec/services/todos/destroy/confidential_issue_service_spec.rb b/spec/services/todos/destroy/confidential_issue_service_spec.rb index ddce45e7968..e3dcc2bae95 100644 --- a/spec/services/todos/destroy/confidential_issue_service_spec.rb +++ b/spec/services/todos/destroy/confidential_issue_service_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Todos::Destroy::ConfidentialIssueService do context 'when provided issue is not confidential' do it 'does not remove any todos' do - issue_1.update(confidential: false) + issue_1.update!(confidential: false) expect { subject }.not_to change { Todo.count } end diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 6de685dd89a..3dafc896b00 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -108,7 +108,7 @@ RSpec.describe Users::DestroyService do context 'projects in pending_delete' do before do project.pending_delete = true - project.save + project.save! end it 'destroys a project in pending_delete' do @@ -310,7 +310,7 @@ RSpec.describe Users::DestroyService do it 'of group_members' do group_member = create(:group_member) - group_member.group.group_members.create(user: user, access_level: 40) + group_member.group.group_members.create!(user: user, access_level: 40) expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once diff --git a/spec/services/users/repair_ldap_blocked_service_spec.rb b/spec/services/users/repair_ldap_blocked_service_spec.rb index b33dcb92f45..54540d68af2 100644 --- a/spec/services/users/repair_ldap_blocked_service_spec.rb +++ b/spec/services/users/repair_ldap_blocked_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Users::RepairLdapBlockedService do describe '#execute' do it 'changes to normal block after destroying last ldap identity' do - identity.destroy + identity.destroy! service.execute expect(user.reload).not_to be_ldap_blocked diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index 29ad85a16ce..ae079229891 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -189,7 +189,7 @@ RSpec.describe VerifyPagesDomainService do let(:domain) { build(:pages_domain, :expired, :with_missing_chain) } before do - domain.save(validate: false) + domain.save!(validate: false) end it 'can be disabled' do diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb index 2924c86c8d0..38c658feba6 100644 --- a/spec/sidekiq/cron/job_gem_dependency_spec.rb +++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Sidekiq::Cron::Job do context 'when Fugit depends on ZoTime or EoTime' do before do described_class - .create(name: 'TestCronWorker', + .create(name: 'TestCronWorker', # rubocop:disable Rails/SaveBang cron: Settings.cron_jobs[:pipeline_schedule_worker]['cron'], class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class']) end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100755..100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100755..100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |