diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index e04e9dfbdfd..a2d5ba5d89b 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -269,7 +269,6 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/serializers/project_serializer_spec.rb' - 'spec/serializers/prometheus_alert_entity_spec.rb' - 'spec/serializers/release_serializer_spec.rb' - - 'spec/serializers/remote_mirror_entity_spec.rb' - 'spec/serializers/review_app_setup_entity_spec.rb' - 'spec/serializers/runner_entity_spec.rb' - 'spec/serializers/serverless/domain_entity_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8ce2e4dc70b..ba51baca2e4 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0f35939596221fc096d873473ef0d6fc3fd09440 +ba02c22370d12ccf8ec464497603394effbaf8b0 diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index c836630d21c..4c2d6ac30bb 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -103,7 +103,9 @@ "workflow": { "type": "object", "properties": { - "name": { "$ref": "#/definitions/workflowName" }, + "name": { + "$ref": "#/definitions/workflowName" + }, "rules": { "type": "array", "items": { @@ -861,98 +863,74 @@ "markdownDescription": "Describes the conditions for when to run the job. Defaults to 'on_success'. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when).", "default": "on_success", "type": "string", - "enum": ["on_success", "on_failure", "always", "never", "manual", "delayed"] + "enum": [ + "on_success", + "on_failure", + "always", + "never", + "manual", + "delayed" + ] }, "cache": { + "markdownDescription": "Use `cache` to specify a list of files and directories to cache between jobs. You can only use paths that are in the local working copy. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cache)", "properties": { - "when": { - "markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).", - "default": "on_success", - "oneOf": [ - { - "enum": [ - "on_success" - ], - "description": "Save the cache only when the job succeeds." - }, - { - "enum": [ - "on_failure" - ], - "description": "Save the cache only when the job fails. " - }, - { - "enum": [ - "always" - ], - "description": "Always save the cache. " - } - ] - } - } - }, - "cache_entry": { - "type": "object", - "description": "Specify files or directories to cache between jobs. Can be set globally or per job.", - "additionalProperties": false, - "properties": { - "paths": { - "type": "array", - "description": "List of files or paths to cache.", - "items": { - "type": "string" - } - }, "key": { + "markdownDescription": "Use the `cache:key` keyword to give each cache a unique identifying key. All jobs that use the same cache key use the same cache, including in different pipelines. Must be used with `cache:path`, or nothing is cached. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekey).", "oneOf": [ { "type": "string", - "description": "Unique cache ID, to allow e.g. specific branch or job cache. Environment variables can be used to set up unique keys (e.g. \"$CI_COMMIT_REF_SLUG\" for per branch cache)." + "pattern": "^(?!.*\\/)^(.*[^.]+.*)$" }, { "type": "object", - "description": "When you include cache:key:files, you must also list the project files that will be used to generate the key, up to a maximum of two files. The cache key will be a SHA checksum computed from the most recent commits (up to two, if two files are listed) that changed the given files.", "properties": { "files": { + "markdownDescription": "Use the `cache:key:files` keyword to generate a new key when one or two specific files change. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekeyfiles)", "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 2 + }, + "prefix": { + "markdownDescription": "Use `cache:key:prefix` to combine a prefix with the SHA computed for `cache:key:files`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekeyprefix)", + "type": "string" } } } ] }, - "untracked": { - "type": "boolean", - "description": "Set to `true` to cache untracked files.", - "default": false + "paths": { + "type": "array", + "markdownDescription": "Use the `cache:paths` keyword to choose which files or directories to cache. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachepaths)", + "items": { + "type": "string" + } }, "policy": { "type": "string", - "description": "Determines the strategy for downloading and updating the cache.", + "markdownDescription": "Determines the strategy for downloading and updating the cache. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachepolicy)", "default": "pull-push", - "oneOf": [ - { - "enum": [ - "pull" - ], - "description": "Pull will download cache but skip uploading after job completes." - }, - { - "enum": [ - "push" - ], - "description": "Push will skip downloading cache and always recreate cache after job completes." - }, - { - "enum": [ - "pull-push" - ], - "description": "Pull-push will both download cache at job start and upload cache on job success." - } + "enum": [ + "pull", + "push", + "pull-push" + ] + }, + "untracked": { + "type": "boolean", + "markdownDescription": "Use `untracked: true` to cache all files that are untracked in your Git repository. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cacheuntracked)", + "default": false + }, + "when": { + "markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).", + "default": "on_success", + "enum": [ + "on_success", + "on_failure", + "always" ] } } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 5111e2609e8..7b0230ff438 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -13,6 +13,7 @@ import StatusIcon from './status_icon.vue'; const FETCH_TYPE_COLLAPSED = 'collapsed'; const FETCH_TYPE_EXPANDED = 'expanded'; +const WIDGET_PREFIX = 'Widget'; export default { components: { @@ -89,6 +90,8 @@ export default { widgetName: { type: String, required: true, + // see https://docs.gitlab.com/ee/development/fe_guide/merge_request_widget_extensions.html#add-new-widgets + validator: (val) => val.startsWith(WIDGET_PREFIX), }, telemetry: { type: Boolean, diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 713231cbc6f..6dd4d72bbc7 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -6,6 +6,7 @@ class ConfirmationsController < Devise::ConfirmationsController include OneTrustCSP include GoogleAnalyticsCSP + skip_before_action :required_signup_info prepend_before_action :check_recaptcha, only: :create before_action :load_recaptcha, only: :new diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 95dd14575e3..020f5cf9d8e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -183,7 +183,11 @@ module Ci end event :succeed do - transition any - [:success] => :success + # A success pipeline can also be retried, for example; a pipeline with a failed manual job. + # When retrying the pipeline, the status of the pipeline is not changed because the failed + # manual job transitions to the `manual` status. + # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98967#note_1144718316 + transition any => :success end event :cancel do diff --git a/app/models/group.rb b/app/models/group.rb index 708fe83a7e5..f33b4fe2942 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1012,10 +1012,6 @@ class Group < Namespace Arel::Nodes::SqlLiteral.new(column_alias)) end - def self.groups_including_descendants_by(group_ids) - Group.where(id: group_ids).self_and_descendants - end - def disable_shared_runners! update!( shared_runners_enabled: false, diff --git a/app/models/todo.rb b/app/models/todo.rb index 634fa9e7eda..f2fa0df852a 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -94,7 +94,7 @@ class Todo < ApplicationRecord # # Returns an `ActiveRecord::Relation`. def for_group_ids_and_descendants(group_ids) - groups = Group.groups_including_descendants_by(group_ids) + groups = Group.where(id: group_ids).self_and_descendants from_union( [ diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index e0e0b82b596..e823b153b23 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -2,7 +2,6 @@ - content_for :page_specific_javascripts do = render "layouts/google_tag_manager_head" = render "layouts/one_trust" - = render "layouts/bizible" = render "layouts/google_tag_manager_body" #signin-container diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml index 8a709a36835..432d2efc36e 100644 --- a/app/views/shared/milestones/_delete_button.html.haml +++ b/app/views/shared/milestones/_delete_button.html.haml @@ -1,11 +1,7 @@ - milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone) -%button.js-delete-milestone-button.btn.gl-button.btn-grouped.btn-danger{ data: { milestone_id: @milestone.id, - milestone_title: markdown_field(@milestone, :title), - milestone_url: milestone_url, - milestone_issue_count: @milestone.issues.count, - milestone_merge_request_count: @milestone.merge_requests.count }, - disabled: true } += render Pajamas::ButtonComponent.new(variant: :danger, + button_options: { class: 'js-delete-milestone-button btn-grouped', data: { milestone_id: @milestone.id, milestone_title: markdown_field(@milestone, :title), milestone_url: milestone_url, milestone_issue_count: @milestone.issues.count, milestone_merge_request_count: @milestone.merge_requests.count }, disabled: true }) do = gl_loading_icon(inline: true, css_class: "gl-mr-2 js-loading-icon hidden") = _('Delete') diff --git a/config/feature_flags/development/cube_api_proxy.yml b/config/feature_flags/development/cube_api_proxy.yml deleted file mode 100644 index 06dcefb1303..00000000000 --- a/config/feature_flags/development/cube_api_proxy.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: cube_api_proxy -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96250 -rollout_issue_url: -milestone: '15.4' -type: development -group: group::product_analytics -default_enabled: false diff --git a/doc/api/commits.md b/doc/api/commits.md index f08fe5ba881..72ec73064dc 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -58,7 +58,7 @@ Example response: "parent_ids": [ "6104942438c14ec7bd21c6cd5bd995272b3faff6" ], - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" }, { "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", @@ -73,7 +73,7 @@ Example response: "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" } ] ``` @@ -173,7 +173,7 @@ Example response: "total": 4 }, "status": null, - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" } ``` @@ -253,7 +253,7 @@ Example response: "total": 25 }, "status": "running", - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/6104942438c14ec7bd21c6cd5bd995272b3faff6" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/6104942438c14ec7bd21c6cd5bd995272b3faff6" } ``` @@ -331,7 +331,7 @@ Example response: "parent_ids": [ "a738f717824ff53aebad8b090c1b79a14f2bd9e8" ], - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/8b090c1b79a14f2bd9e8a738f717824ff53aebad" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/8b090c1b79a14f2bd9e8a738f717824ff53aebad" } ``` @@ -401,7 +401,7 @@ Example response: "committer_name":"Administrator", "committer_email":"admin@example.com", "committed_date":"2018-11-08T15:55:26.000Z", - "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/8b090c1b79a14f2bd9e8a738f717824ff53aebad" + "web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/commit/8b090c1b79a14f2bd9e8a738f717824ff53aebad" } ``` @@ -545,11 +545,11 @@ Example response: ```json { "author" : { - "web_url" : "https://gitlab.example.com/thedude", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", - "username" : "thedude", + "web_url" : "https://gitlab.example.com/janedoe", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png", + "username" : "janedoe", "state" : "active", - "name" : "Jeff Lebowski", + "name" : "Jane Doe", "id" : 28 }, "created_at" : "2016-01-19T09:44:55.600Z", @@ -590,15 +590,15 @@ Example response: { "id": 334686748, "type": null, - "body": "I'm the Dude, so that's what you call me.", + "body": "Nice piece of code!", "attachment": null, "author" : { "id" : 28, - "name" : "Jeff Lebowski", - "username" : "thedude", - "web_url" : "https://gitlab.example.com/thedude", + "name" : "Jane Doe", + "username" : "janedoe", + "web_url" : "https://gitlab.example.com/janedoe", "state" : "active", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png" }, "created_at": "2020-04-30T18:48:11.432Z", "updated_at": "2020-04-30T18:48:11.432Z", @@ -655,16 +655,16 @@ Example response: "name" : "bundler:audit", "allow_failure" : true, "author" : { - "username" : "thedude", + "username" : "janedoe", "state" : "active", - "web_url" : "https://gitlab.example.com/thedude", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", + "web_url" : "https://gitlab.example.com/janedoe", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png", "id" : 28, - "name" : "Jeff Lebowski" + "name" : "Jane Doe" }, "description" : null, "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8", - "target_url" : "https://gitlab.example.com/thedude/gitlab-foss/builds/91", + "target_url" : "https://gitlab.example.com/janedoe/gitlab-foss/builds/91", "finished_at" : null, "id" : 91, "ref" : "master" @@ -675,18 +675,18 @@ Example response: "allow_failure" : false, "status" : "pending", "created_at" : "2016-01-19T08:40:25.832Z", - "target_url" : "https://gitlab.example.com/thedude/gitlab-foss/builds/90", + "target_url" : "https://gitlab.example.com/janedoe/gitlab-foss/builds/90", "id" : 90, "finished_at" : null, "ref" : "master", "sha" : "18f3e63d05582537db6d183d9d557be09e1f90c8", "author" : { "id" : 28, - "name" : "Jeff Lebowski", - "username" : "thedude", - "web_url" : "https://gitlab.example.com/thedude", + "name" : "Jane Doe", + "username" : "janedoe", + "web_url" : "https://gitlab.example.com/janedoe", "state" : "active", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png" }, "description" : null }, @@ -724,10 +724,10 @@ Example response: ```json { "author" : { - "web_url" : "https://gitlab.example.com/thedude", - "name" : "Jeff Lebowski", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", - "username" : "thedude", + "web_url" : "https://gitlab.example.com/janedoe", + "name" : "Jane Doe", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png", + "username" : "janedoe", "state" : "active", "id" : 28 }, @@ -781,10 +781,10 @@ Example response: "upvotes":0, "downvotes":0, "author" : { - "web_url" : "https://gitlab.example.com/thedude", - "name" : "Jeff Lebowski", - "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", - "username" : "thedude", + "web_url" : "https://gitlab.example.com/janedoe", + "name" : "Jane Doe", + "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/jane-doe-400-400.png", + "username" : "janedoe", "state" : "active", "id" : 28 }, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 89c3680ec7c..b7a3a5c6b3f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7582,6 +7582,29 @@ The edge type for [`ExternalAuditEventDestination`](#externalauditeventdestinati | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`ExternalAuditEventDestination`](#externalauditeventdestination) | The item at the end of the edge. | +#### `ExternalStatusCheckConnection` + +The connection type for [`ExternalStatusCheck`](#externalstatuscheck). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[ExternalStatusCheckEdge]`](#externalstatuscheckedge) | A list of edges. | +| `nodes` | [`[ExternalStatusCheck]`](#externalstatuscheck) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `ExternalStatusCheckEdge` + +The edge type for [`ExternalStatusCheck`](#externalstatuscheck). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`ExternalStatusCheck`](#externalstatuscheck) | The item at the end of the edge. | + #### `GroupConnection` The connection type for [`Group`](#group). @@ -10461,6 +10484,7 @@ List of branch rules for a project, grouped by branch name. | `approvalRules` | [`ApprovalProjectRuleConnection`](#approvalprojectruleconnection) | Merge request approval rules configured for this branch rule. (see [Connections](#connections)) | | `branchProtection` | [`BranchProtection!`](#branchprotection) | Branch protections configured for this branch rule. | | `createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. | +| `externalStatusChecks` | [`ExternalStatusCheckConnection`](#externalstatuscheckconnection) | External status checks configured for this branch rule. (see [Connections](#connections)) | | `isDefault` | [`Boolean!`](#boolean) | Check if this branch rule protects the project's default branch. | | `name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. | | `updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. | @@ -12636,6 +12660,18 @@ Represents an external issue. | `updatedAt` | [`Time`](#time) | Timestamp of when the issue was updated. | | `webUrl` | [`String`](#string) | URL to the issue in the external tracker. | +### `ExternalStatusCheck` + +Describes an external status check. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `externalUrl` | [`String!`](#string) | External URL for the status check. | +| `id` | [`GlobalID!`](#globalid) | ID of the rule. | +| `name` | [`String!`](#string) | Name of the rule. | + ### `FileUpload` #### Fields diff --git a/doc/tutorials/agile_sprint.md b/doc/tutorials/agile_sprint.md new file mode 100644 index 00000000000..69fa9c8fb07 --- /dev/null +++ b/doc/tutorials/agile_sprint.md @@ -0,0 +1,101 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Use GitLab to run an agile iteration + +To run an agile development iteration in GitLab, you use multiple GitLab features +that work together. + +To run an agile iteration from GitLab: + +1. Create a group. +1. Create a project. +1. Set up an iteration cadence. +1. Create scoped labels. +1. Create your epics and issues. +1. Create an issue board. + +After you've created these core components, you can begin running your iterations. + +## Create a group + +Iteration cadences are created at the group level, so start by +[creating one](../user/group/manage.md#create-a-group) if you don't have one already. + +You use groups to manage one or more related projects at the same time. +You add your users as members in the group, and assign them a role. Roles determine +the [level of permissions](../user/permissions.md) each user has on the projects in the group. +Membership automatically cascades down to all subgroups and projects. + +## Create a project + +Now [create one or more projects](../user/project/working_with_projects.md#create-a-project) in your group. +There are several different ways to create a project. A project contains +your code and pipelines, but also the issues that are used for planning your upcoming code changes. + +## Set up an iteration cadence + +Before you start creating epics or issues, create an +[iteration cadence](../user/group/iterations/index.md#iteration-cadences). +Iteration cadences contain the individual, sequential iteration timeboxes for planning and reporting +on your issues. + +When creating an iteration cadence, you can decide whether to automatically manage the iterations or +disable the automated scheduling to +[manually manage the iterations](../user/group/iterations/index.md#manual-iteration-management). + +Similar to membership, iterations cascade down your group, subgroup, and project hierarchy. If your +team works across many groups, subgroups, and projects, create the iteration cadence in the top-most +group shared by all projects that contain the team's issues as illustrated by the diagram below. + +```mermaid +graph TD + Group --> SubgroupA --> Project1 + Group --> SubgroupB --> Project2 + Group --> IterationCadence +``` + +## Create scoped labels + +You should also [create scoped labels](../user/project/labels.md) in the same group where you created +your iteration cadence. Labels help you +organize your epics, issues, and merge requests, as well as help you +to visualize the flow of issues in boards. For example, you can use scoped labels like +`workflow::planning`, `workflow::ready for development`, `workflow::in development`, and `workflow::complete` +to indicate the status of an issue. You can also leverage scoped labels to denote the type of issue +or epic such as `type::feature`, `type::defect`, and `type::maintenance`. + +## Create your epics and issues + +Now you can get started planning your iterations. Start by creating [epics](../user/group/epics/index.md) +in the group where you created your iteration cadence, +then create child [issues](../user/project/issues/index.md) in one or more of your projects. +Add labels to each as needed. + +## Create an issue board + +[Issue boards](../user/project/issue_board.md) help you plan your upcoming iterations or visualize +the workflow of the iteration currently in progress. List columns can be created based on label, +assignee, iteration, or milestone. You can also filter the board by multiple attributes and group +issues by their epic. + +In the group where you created your iteration cadence and labels, +[create an issue board](../user/project/issue_board.md#create-an-issue-board) and name it +"Iteration Planning." Then, create lists for each of your iterations. You can then drag issues from +the "Open" list into iteration lists to schedule them for upcoming iterations. + +To visualize the workflow for issues in the current iteration, create another issue board called +"Current Iteration." When you're creating the board: + +1. Select **Edit board**. +1. Next to **Iteration**, select **Edit**. +1. From the dropdown list, select **Current iteration**. +1. Select **Save changes**. + +Your board will now only ever show issues that are in the current iteration. +You can start adding lists for each of the `workflow::...` labels you created previously. + +Now you're ready to start development. diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md index e60ca2878a5..0ed6335c89a 100644 --- a/doc/tutorials/index.md +++ b/doc/tutorials/index.md @@ -44,6 +44,8 @@ collaborating, and more. |-------|-------------|--------------------| | [Create a project from a template](https://gitlab.com/projects/new#create_from_template) | For hands-on learning, select **Sample GitLab Project** and create a project with example issues and merge requests. | **{star}** | | [Migrate to GitLab](../user/project/import/index.md) | If you are coming to GitLab from another platform, you can import or convert your projects. | | +| [Run an agile iteration](agile_sprint.md) | Use group, projects, and iterations to run an agile development iteration. | +| [Use GitLab for multi-team planning (SAFe)](https://www.youtube.com/watch?v=KmASFwSap7c) (37m 37s) | A use case of a multi-team organization that uses GitLab with [Scaled Agile Framework (SAFe)](https://about.gitlab.com/solutions/agile-delivery/scaled-agile/). | ## Use CI/CD pipelines diff --git a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb index 3180289ec69..737852d5ccb 100644 --- a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb +++ b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb @@ -4,7 +4,7 @@ module Gitlab module Database module LoadBalancing class SidekiqServerMiddleware - JobReplicaNotUpToDate = Class.new(StandardError) + JobReplicaNotUpToDate = Class.new(::Gitlab::SidekiqMiddleware::RetryError) MINIMUM_DELAY_INTERVAL_SECONDS = 0.8 diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 83920182da4..582c3380869 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -131,6 +131,9 @@ module Gitlab end def before_send(event, hint) + # Don't report Sidekiq retry errors to Sentry + return if hint[:exception].is_a?(Gitlab::SidekiqMiddleware::RetryError) + inject_context_for_exception(event, hint[:exception]) custom_fingerprinting(event, hint[:exception]) diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb index 2c9b79bfc9d..d248ad2eeca 100644 --- a/lib/gitlab/json.rb +++ b/lib/gitlab/json.rb @@ -167,23 +167,11 @@ module Gitlab # @return [Boolean, String, Array, Hash, Object] # @raise [JSON::ParserError] def handle_legacy_mode!(data) - return data unless feature_table_exists? + return data unless Feature.feature_flags_available? return data unless Feature.enabled?(:json_wrapper_legacy_mode) raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) } end - - # There are a variety of database errors possible when checking the feature - # flags at the wrong time during boot, e.g. during migrations. We don't care - # about these errors, we just need to ensure that we skip feature detection - # if they will fail. - # - # @return [Boolean] - def feature_table_exists? - Feature::FlipperFeature.table_exists? - rescue StandardError - false - end end # GrapeFormatter is a JSON formatter for the Grape API. diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb index 258c904290d..d5e80053772 100644 --- a/lib/gitlab/request_forgery_protection.rb +++ b/lib/gitlab/request_forgery_protection.rb @@ -10,6 +10,14 @@ module Gitlab class Controller < ActionController::Base protect_from_forgery with: :exception, prepend: true + def initialize + super + + # Squelch noisy and unnecessary "Can't verify CSRF token authenticity." messages. + # X-Csrf-Token is only one authentication mechanism for API helpers. + self.logger = ActiveSupport::Logger.new(File::NULL) + end + def index head :ok end diff --git a/lib/gitlab/sidekiq_middleware/retry_error.rb b/lib/gitlab/sidekiq_middleware/retry_error.rb new file mode 100644 index 00000000000..372213a8e6a --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/retry_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + # Sidekiq retry error that won't be reported to Sentry + # Use it when a job retry is an expected behavior + RetryError = Class.new(StandardError) + end +end diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb index 111bfb24c7e..773a416dcb4 100644 --- a/spec/controllers/confirmations_controller_spec.rb +++ b/spec/controllers/confirmations_controller_spec.rb @@ -10,17 +10,27 @@ RSpec.describe ConfirmationsController do end describe '#show' do + let_it_be_with_reload(:user) { create(:user, :unconfirmed) } + let(:confirmation_token) { user.confirmation_token } + render_views def perform_request get :show, params: { confirmation_token: confirmation_token } end + context 'when signup info is required' do + before do + allow(controller).to receive(:current_user) { user } + user.set_role_required! + end + + it 'does not redirect' do + expect(perform_request).not_to redirect_to(users_sign_up_welcome_path) + end + end + context 'user is already confirmed' do - let_it_be_with_reload(:user) { create(:user, :unconfirmed) } - - let(:confirmation_token) { user.confirmation_token } - before do user.confirm end @@ -57,10 +67,6 @@ RSpec.describe ConfirmationsController do end context 'user accesses the link after the expiry of confirmation token has passed' do - let_it_be_with_reload(:user) { create(:user, :unconfirmed) } - - let(:confirmation_token) { user.confirmation_token } - before do allow(Devise).to receive(:confirm_within).and_return(1.day) end @@ -133,6 +139,17 @@ RSpec.describe ConfirmationsController do stub_feature_flags(identity_verification: false) end + context 'when signup info is required' do + before do + allow(controller).to receive(:current_user) { user } + user.set_role_required! + end + + it 'does not redirect' do + expect(perform_request).not_to redirect_to(users_sign_up_welcome_path) + end + end + context 'when reCAPTCHA is disabled' do before do stub_application_setting(recaptcha_enabled: false) diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 0b468854322..306888b9ab8 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -139,7 +139,7 @@ RSpec.describe 'Dashboard Projects' do end describe 'with a pipeline', :clean_gitlab_redis_shared_state do - let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) } before do # Since the cache isn't updated when a new pipeline is created diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index 3d843e8fb81..b2c9118ddcf 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -35,7 +35,6 @@ import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml'; // YAML NEGATIVE TEST import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; -import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml'; import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml'; import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml'; import VariablesNegativeYaml from './yaml_tests/negative_tests/variables.yml'; @@ -62,6 +61,16 @@ import ProjectPathTriggerProjectLeadSlashYaml from './yaml_tests/negative_tests/ import ProjectPathTriggerProjectNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml'; import ProjectPathTriggerProjectTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml'; +import CacheKeyFilesNotArray from './yaml_tests/negative_tests/cache/key_files_not_an_array.yml'; +import CacheKeyPrefixArray from './yaml_tests/negative_tests/cache/key_prefix_array.yml'; +import CacheKeyWithDot from './yaml_tests/negative_tests/cache/key_with_dot.yml'; +import CacheKeyWithMultipleDots from './yaml_tests/negative_tests/cache/key_with_multiple_dots.yml'; +import CacheKeyWithSlash from './yaml_tests/negative_tests/cache/key_with_slash.yml'; +import CachePathsNotAnArray from './yaml_tests/negative_tests/cache/paths_not_an_array.yml'; +import CacheUntrackedString from './yaml_tests/negative_tests/cache/untracked_string.yml'; +import CacheWhenInteger from './yaml_tests/negative_tests/cache/when_integer.yml'; +import CacheWhenNotReservedKeyword from './yaml_tests/negative_tests/cache/when_not_reserved_keyword.yml'; + const ajv = new Ajv({ strictTypes: false, strictTuples: false, @@ -116,7 +125,15 @@ describe('negative tests', () => { // YAML ArtifactsNegativeYaml, - CacheNegativeYaml, + CacheKeyFilesNotArray, + CacheKeyPrefixArray, + CacheKeyWithDot, + CacheKeyWithMultipleDots, + CacheKeyWithSlash, + CachePathsNotAnArray, + CacheUntrackedString, + CacheWhenInteger, + CacheWhenNotReservedKeyword, IncludeNegativeYaml, JobWhenNegativeYaml, RulesNegativeYaml, diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml deleted file mode 100644 index 04020c06753..00000000000 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml +++ /dev/null @@ -1,13 +0,0 @@ -stages: - - prepare - -# invalid cache:when values -when no integer: - stage: prepare - cache: - when: 0 - -when must be a reserved word: - stage: prepare - cache: - when: 'never' diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_files_not_an_array.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_files_not_an_array.yml new file mode 100644 index 00000000000..64b927a9940 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_files_not_an_array.yml @@ -0,0 +1,8 @@ +cache-key-files-not-an-array: + script: echo "This job uses a cache." + cache: + key: + files: package.json + paths: + - vendor/ruby + - node_modules diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_prefix_array.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_prefix_array.yml new file mode 100644 index 00000000000..9024dfe6441 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_prefix_array.yml @@ -0,0 +1,10 @@ +cache-key-prefix-array: + script: echo "This job uses a cache." + cache: + key: + files: + - Gemfile.lock + prefix: + - binaries-cache-$CI_JOB_NAME + paths: + - binaries/ diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_dot.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_dot.yml new file mode 100644 index 00000000000..7d21e5f4111 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_dot.yml @@ -0,0 +1,6 @@ +cache-key-with-.: + script: echo "This job uses a cache." + cache: + key: . + paths: + - binaries/ diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_multiple_dots.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_multiple_dots.yml new file mode 100644 index 00000000000..1256be628d1 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_multiple_dots.yml @@ -0,0 +1,7 @@ +cache-key-with-multiple-.: + stage: test + script: echo "This job uses a cache." + cache: + key: .. + paths: + - binaries/ diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_slash.yml new file mode 100644 index 00000000000..ea6c0345bd4 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/key_with_slash.yml @@ -0,0 +1,6 @@ +cache-key-with-/: + script: echo "This job uses a cache." + cache: + key: binaries-ca/che + paths: + - binaries/ diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/paths_not_an_array.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/paths_not_an_array.yml new file mode 100644 index 00000000000..26cc8d1935e --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/paths_not_an_array.yml @@ -0,0 +1,5 @@ +cache-path-not-an-array: + script: echo "This job uses a cache." + cache: + key: binaries-cache + paths: binaries/*.apk diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/untracked_string.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/untracked_string.yml new file mode 100644 index 00000000000..ed21e87f009 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/untracked_string.yml @@ -0,0 +1,4 @@ +cache-untracked-string: + script: echo "This job uses a cache." + cache: + untracked: 'true' diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_integer.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_integer.yml new file mode 100644 index 00000000000..5420bd9d0dd --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_integer.yml @@ -0,0 +1,4 @@ +when_integer: + script: echo "This job uses a cache." + cache: + when: 0 diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_not_reserved_keyword.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_not_reserved_keyword.yml new file mode 100644 index 00000000000..2a6e204a6db --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache/when_not_reserved_keyword.yml @@ -0,0 +1,4 @@ +when_not_reserved_keyword: + script: echo "This job uses a cache." + cache: + when: 'never' diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml index d83e14fdc6a..75918cd2a1b 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml @@ -1,24 +1,124 @@ -stages: - - prepare - # valid cache:when values job1: - stage: prepare script: - echo 'running job' cache: when: 'on_success' job2: - stage: prepare script: - echo 'running job' cache: when: 'on_failure' job3: - stage: prepare script: - echo 'running job' cache: when: 'always' + +# valid cache:paths +cache-paths: + script: echo "This job uses a cache." + cache: + key: binaries-cache + paths: + - binaries/*.apk + - .config + +# valid cache:key +cache-key-string: + script: echo "This job uses a cache." + cache: + key: random-string + paths: + - binaries/ + +cache-key-string-with-dots: + script: echo "This job uses a cache." + cache: + key: random-..string + paths: + - binaries/ + +cache-key-string-beginning-with-dot: + script: echo "This job uses a cache." + cache: + key: .random-string + paths: + - binaries/ + +cache-key-string-ending-with-dot: + script: echo "This job uses a cache." + cache: + key: random-string. + paths: + - binaries/ + +cache-key-predefined-variable: + script: echo "This job uses a cache." + cache: + key: $CI_COMMIT_REF_SLUG + paths: + - binaries/ + +cache-key-combination: + script: echo "This job uses a cache." + cache: + key: binaries-cache-$CI_COMMIT_REF_SLUG + paths: + - binaries/ + +# valid cache:key:files +cache-key-files: + script: echo "This job uses a cache." + cache: + key: + files: + - Gemfile.lock + - package.json + paths: + - vendor/ruby + - node_modules + +# valide cache:key:prefix +cache-key-prefix-string: + script: echo "This job uses a cache." + cache: + key: + files: + - Gemfile.lock + prefix: random-string + paths: + - binaries/ + +cache-key-prefix-predefined-variable: + script: echo "This job uses a cache." + cache: + key: + files: + - Gemfile.lock + prefix: $CI_JOB_NAME + paths: + - binaries/ + +cache-key-prefix-combination: + script: echo "This job uses a cache." + cache: + key: + files: + - Gemfile.lock + prefix: binaries-cache-$CI_JOB_NAME + paths: + - binaries/ + +# valid cache:untracked +cache-untracked-true: + script: test + cache: + untracked: true + +cache-untracked-false: + script: test + cache: + untracked: false diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js index b17239384a7..814a1e68142 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -28,7 +28,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { propsData: { isCollapsible: false, loadingText: 'Loading widget', - widgetName: 'MyWidget', + widgetName: 'WidgetTest', value: { collapsed: null, expanded: null, @@ -94,6 +94,14 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { await nextTick(); expect(wrapper.text()).toContain('Loading'); }); + + it('validates widget name', () => { + expect(() => { + createComponent({ + propsData: { fetchCollapsedData: jest.fn(), widgetName: 'InvalidWidgetName' }, + }); + }).toThrow(); + }); }); describe('fetch', () => { diff --git a/spec/graphql/types/projects/branch_rule_type_spec.rb b/spec/graphql/types/projects/branch_rule_type_spec.rb index 119ecf8a097..41923ebb6a9 100644 --- a/spec/graphql/types/projects/branch_rule_type_spec.rb +++ b/spec/graphql/types/projects/branch_rule_type_spec.rb @@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['BranchRule'] do ] end - specify { is_expected.to require_graphql_authorizations(:read_protected_branch) } + it { is_expected.to require_graphql_authorizations(:read_protected_branch) } - specify { is_expected.to have_graphql_fields(fields).at_least } + it { is_expected.to have_graphql_fields(fields).at_least } end diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index fd859ae40fb..4900547e9e9 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -369,6 +369,25 @@ RSpec.describe Gitlab::ErrorTracking do end end + context 'when exception is excluded' do + before do + stub_const('SubclassRetryError', Class.new(Gitlab::SidekiqMiddleware::RetryError)) + end + + ['Gitlab::SidekiqMiddleware::RetryError', 'SubclassRetryError'].each do |ex| + let(:exception) { ex.constantize.new } + + it "does not report #{ex} exception to Sentry" do + expect(Gitlab::ErrorTracking::Logger).to receive(:error) + + track_exception + + expect(Raven.client.transport.events).to eq([]) + expect(Sentry.get_current_client.transport.events).to eq([]) + end + end + end + context 'when processing invalid URI exceptions' do let(:invalid_uri) { 'http://foo:bar' } let(:raven_exception_values) { raven_event['exception']['values'] } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c11927150e4..53baad9f99a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -367,6 +367,7 @@ protected_branches: - push_access_levels - unprotect_access_levels - approval_project_rules +- external_status_checks - required_code_owners_sections protected_tags: - project diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb index a7b777cf4f2..fe93fe9c828 100644 --- a/spec/lib/gitlab/request_forgery_protection_spec.rb +++ b/spec/lib/gitlab/request_forgery_protection_spec.rb @@ -13,6 +13,18 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do } end + it 'logs to /dev/null' do + logger = described_class::Controller.new.logger + + # Taken from ActiveSupport.logger_outputs_to? + # There is no equivalent /dev/null stream like STDOUT, so + # we need to extract the path. + logdev = logger.instance_variable_get(:@logdev) + logger_source = logdev.dev + + expect(logger_source.path).to eq(File::NULL) + end + describe '.call' do context 'when the request method is GET' do before do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 6a5162a85ae..7efaa9ebdd5 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -166,7 +166,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it do pipeline.status = from_status.to_s - if from_status != to_status + if from_status != to_status || success_to_success? expect(pipeline.set_status(to_status.to_s)) .to eq(true) else @@ -174,6 +174,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do .to eq(false), "loopback transitions are not allowed" end end + + private + + def success_to_success? + from_status == :success && to_status == :success + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 928f9d6d057..88b13a1c0ea 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -2406,23 +2406,6 @@ RSpec.describe Group do end end - describe '.groups_including_descendants_by' do - let_it_be(:parent_group1) { create(:group) } - let_it_be(:parent_group2) { create(:group) } - let_it_be(:extra_group) { create(:group) } - let_it_be(:child_group1) { create(:group, parent: parent_group1) } - let_it_be(:child_group2) { create(:group, parent: parent_group1) } - let_it_be(:child_group3) { create(:group, parent: parent_group2) } - - subject { described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id]) } - - shared_examples 'returns the expected groups for a group and its descendants' do - specify { is_expected.to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3) } - end - - it_behaves_like 'returns the expected groups for a group and its descendants' - end - describe '.preset_root_ancestor_for' do let_it_be(:rootgroup, reload: true) { create(:group) } let_it_be(:subgroup, reload: true) { create(:group, parent: rootgroup) } diff --git a/spec/serializers/remote_mirror_entity_spec.rb b/spec/serializers/remote_mirror_entity_spec.rb index 4cbf87e4d67..c6290e15995 100644 --- a/spec/serializers/remote_mirror_entity_spec.rb +++ b/spec/serializers/remote_mirror_entity_spec.rb @@ -3,8 +3,7 @@ require 'spec_helper' RSpec.describe RemoteMirrorEntity do - let(:project) { create(:project, :repository, :remote_mirror, url: "https://test:password@gitlab.com") } - let(:remote_mirror) { project.remote_mirrors.first } + let(:remote_mirror) { build_stubbed(:remote_mirror, url: "https://test:password@gitlab.com") } let(:entity) { described_class.new(remote_mirror) } subject { entity.as_json } diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 9a37736967e..1fbefc1fa22 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do check_expectation(test_file.dig('init', 'expect'), "init") test_file['transitions'].each_with_index do |transition, idx| - event_on_jobs(transition['event'], transition['jobs']) + process_events(transition) Sidekiq::Worker.drain_all # ensure that all async jobs are executed check_expectation(transition['expect'], "transition:#{idx}") end @@ -48,6 +48,14 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do } end + def process_events(transition) + if transition['jobs'] + event_on_jobs(transition['event'], transition['jobs']) + else + event_on_pipeline(transition['event']) + end + end + def event_on_jobs(event, job_names) statuses = pipeline.latest_statuses.by_name(job_names).to_a expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts @@ -63,6 +71,14 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do end end end + + def event_on_pipeline(event) + if event == 'retry' + pipeline.retry_failed(user) + else + pipeline.public_send("#{event}!") + end + end end end diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml new file mode 100644 index 00000000000..a50fe56f8d4 --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml @@ -0,0 +1,54 @@ +config: + test1: + script: exit 0 + + test2: + when: manual + script: exit 1 + +init: + expect: + pipeline: pending + stages: + test: pending + jobs: + test1: pending + test2: manual + +transitions: + - event: success + jobs: [test1] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: manual + - event: play + jobs: [test2] + expect: + pipeline: running + stages: + test: running + jobs: + test1: success + test2: pending + - event: drop + jobs: [test2] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: failed + - event: retry + jobs: [test2] + expect: + pipeline: running + stages: + test: running + jobs: + test1: success + test2: pending diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml new file mode 100644 index 00000000000..a6112a95a12 --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml @@ -0,0 +1,53 @@ +config: + test1: + script: exit 0 + + test2: + when: manual + script: exit 1 + +init: + expect: + pipeline: pending + stages: + test: pending + jobs: + test1: pending + test2: manual + +transitions: + - event: success + jobs: [test1] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: manual + - event: play + jobs: [test2] + expect: + pipeline: running + stages: + test: running + jobs: + test1: success + test2: pending + - event: drop + jobs: [test2] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: failed + - event: retry + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: manual diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 96437290ae3..77345096537 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -5,14 +5,16 @@ require 'spec_helper' RSpec.describe Ci::RetryPipelineService, '#execute' do include ProjectForksHelper - let(:user) { create(:user) } - let(:project) { create(:project) } + let_it_be_with_refind(:user) { create(:user) } + let_it_be_with_refind(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } - let(:service) { described_class.new(project, user) } let(:build_stage) { create(:ci_stage, name: 'build', position: 0, pipeline: pipeline) } let(:test_stage) { create(:ci_stage, name: 'test', position: 1, pipeline: pipeline) } let(:deploy_stage) { create(:ci_stage, name: 'deploy', position: 2, pipeline: pipeline) } + subject(:service) { described_class.new(project, user) } + context 'when user has full ability to modify pipeline' do before do project.add_developer(user) @@ -272,6 +274,21 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do expect(pipeline.reload).to be_running end end + + context 'when there is a failed manual action' do + before do + create_build('rspec', :success, build_stage) + create_build('manual-rspec', :failed, build_stage, when: :manual, allow_failure: true) + end + + it 'processes the manual action' do + service.execute(pipeline) + + expect(build('rspec')).to be_success + expect(build('manual-rspec')).to be_manual + expect(pipeline.reload).to be_success + end + end end it 'closes all todos about failed jobs for pipeline' do