diff options
141 files changed, 2490 insertions, 558 deletions
diff --git a/.gitlab/issue_templates/Doc Review.md b/.gitlab/issue_templates/Doc Review.md new file mode 100644 index 00000000000..14ab0c82d59 --- /dev/null +++ b/.gitlab/issue_templates/Doc Review.md @@ -0,0 +1,20 @@ +<!-- This issue requests a technical writer review as required for documentation + content that was merged without one. --> + +<!-- NOTE: Please add a DevOps stage label (format `devops:<stage_name>`) + and assign the technical writer who is + [listed for that stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). --> + + +## References + +Merged MR that introduced documentation requiring review: + +Related issue(s): + +## Further Details + +<!-- Any additional context, questions, or notes for the technical writer. --> + + +/label ~Documentation ~docs-review diff --git a/.gitlab/issue_templates/Documentation.md b/.gitlab/issue_templates/Documentation.md index b33ed5bcaa8..c0919aeeda4 100644 --- a/.gitlab/issue_templates/Documentation.md +++ b/.gitlab/issue_templates/Documentation.md @@ -1,54 +1,53 @@ -<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> +<!-- -<!-- Mention "documentation" or "docs" in the issue title --> +* Use this issue template for suggesting new docs or updates to existing docs. + Note: Doc work as part of feature development is covered in the Feature Request template. + +* For issues related to features of the docs.gitlab.com site, see + https://gitlab.com/gitlab-com/gitlab-docs/issues/ -<!-- Use this description template for new docs or updates to existing docs. --> +* For information about documentation content and process, see + https://docs.gitlab.com/ee/development/documentation/ --> -<!-- Check the documentation structure guidelines for guidance: https://docs.gitlab.com/ee/development/documentation/structure.html--> +### Type of issue -- [ ] Documents Feature A <!-- feature name --> -- [ ] Follow-up from: #XXX, !YYY <!-- Mention related issues, MRs, and epics when available --> +<!-- Un-comment the line for the applicable doc issue type to add its label. + Note that all text on that line is deleted upon issue creation. --> +<!-- /label ~"docs:fix" - Correction or clarification needed. --> +<!-- /label ~"docs:new" - New doc needed to cover a new topic or use case. --> +<!-- /label ~"docs:improvement" - Improving an existing doc; e.g. adding a diagram, adding or rewording text, resolving redundancies, cross-linking, etc. --> +<!-- /label ~"docs:revamp" - Review a page or group of pages in order to plan and implement major improvements/rewrites. --> +<!-- /label ~"docs:other" - Anything else. --> -## New doc or update? +### Problem to solve -<!-- Mark either of these boxes: --> +<!-- Include the following detail as necessary: +* What product or feature(s) affected? +* What docs or doc section affected? Include links or paths. +* Is there a problem with a specific document, or a feature/process that's not addressed sufficiently in docs? +* Any other ideas or requests? +--> -- [ ] New documentation -- [ ] Update existing documentation +### Further details -## Checklists +<!-- +* Any concepts, procedures, reference info we could add to make it easier to successfully use GitLab? +* Include use cases, benefits, and/or goals for this work. +* If adding content: What audience is it intended for? (What roles and scenarios?) + For ideas, see personas at https://design.gitlab.com/research/personas or the persona labels at + https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A +--> -### Product Manager +### Proposal -<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#1-product-manager-s-role-in-the-documentation-process --> +<!-- Further specifics for how can we solve the problem. --> -- [ ] Add the correct labels -- [ ] Add the correct milestone -- [ ] Indicate the correct document/directory for this feature <!-- (ping the tech writers for help if you're not sure) --> -- [ ] Fill the doc blurb below +### Who can address the issue -#### Documentation blurb +<!-- What if any special expertise is required to resolve this issue? --> -<!-- Documentation template: https://docs.gitlab.com/ee/development/documentation/structure.html#documentation-template-for-new-docs --> +### Other links/references -- Doc **title** - - <!-- write the doc title here --> - -- Feature **overview/description** - - <!-- Write the feature overview here --> - -- Feature **use cases** - - <!-- Write the use cases here --> - -### Developer - -<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process --> - -- [ ] Copy the doc blurb above and paste it into the doc -- [ ] Write the tutorial - explain how to use the feature -- [ ] Submit the MR using the appropriate MR description template +<!-- E.g. related GitLab issues/MRs --> /label ~Documentation diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 1bb8d33ff63..abfbef4b2b0 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -1,6 +1,6 @@ ### Problem to solve -<!--- What problem do we solve? --> +<!-- What problem do we solve? --> ### Target audience @@ -31,15 +31,20 @@ Existing personas are: (copy relevant personas out of this comment, and delete a ### Further details -<!--- Include use cases, benefits, and/or goals (contributes to our vision?) --> +<!-- Include use cases, benefits, and/or goals (contributes to our vision?) --> ### Proposal -<!--- How are we going to solve the problem? Try to include the user journey! --> +<!-- How are we going to solve the problem? Try to include the user journey! https://about.gitlab.com/handbook/journeys/#user-journey --> + +### Documentation + +<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html +Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements --> ### What does success look like, and how can we measure that? -<!--- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this --> +<!-- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this. --> ### Links / references diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 4457821e23e..ba9624aeeab 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -1,33 +1,39 @@ -<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> +<!-- Follow the documentation workflow https://docs.gitlab.com/ee/development/documentation/workflow.html --> +<!-- Additional information is located at https://docs.gitlab.com/ee/development/documentation/ --> <!-- Mention "documentation" or "docs" in the MR title --> - -<!-- Use this description template for new docs or updates to existing docs. For changing documentation location use the "Change documentation location" template --> +<!-- For changing documentation location use the "Change documentation location" template --> ## What does this MR do? -<!-- Briefly describe what this MR is about --> +<!-- Briefly describe what this MR is about. --> ## Related issues -<!-- Mention the issue(s) this MR closes or is related to --> - -Closes +<!-- Link related issues below. Insert the issue link or reference after the word "Closes" if merging this should automatically close it. --> ## Author's checklist -- [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#1-product-managers-role) -- [ ] Crosslink the document from the higher-level index -- [ ] Crosslink the document from other subject-related docs -- [ ] Feature moving tiers? Make sure the change is also reflected in [`features.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml) -- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers) -- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_ +- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). +- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful. +- [ ] Apply the ~Documentation label. ## Review checklist -- [ ] Your team's review (required) -- [ ] PM's review (recommended, but not a blocker) -- [ ] Technical writer's review (required) -- [ ] Merge the EE-MR first, CE-MR afterwards +All reviewers can help ensure accuracy, clarity, completeness, and adherence to the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). + +**1. Primary Reviewer** + +* [ ] Review by a code reviewer or other selected colleague to confirm accuracy, clarity, and completeness. This can be skipped for minor fixes without substantive content changes. + +**2. Technical Writer** + +* [ ] Optional: Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). + +**3. Maintainer** + +1. [ ] Review by assigned maintainer, who can always request/require the above reviews. Maintainer's review can occur before or after a technical writer review. +1. [ ] Ensure a release milestone is set and that you merge the equivalent EE MR before the CE MR if both exist. +1. [ ] If there has not been a technical writer review, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review). /label ~Documentation diff --git a/PROCESS.md b/PROCESS.md index 7c3c826f921..1f99cebe081 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -168,8 +168,12 @@ on behalf of the community member. Every new feature or change should be shipped with its corresponding documentation in accordance with the -[documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html) -and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html). +[documentation process](https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html) +and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html) guides. +Note that a technical writer will review all changes to documentation. This can occur +in the same MR as the feature code, but [if there is not sufficient time or need, +it can be planned via a follow-up issue for doc review](https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#1-product-managers-role), +and another MR, if needed. Regardless, complete docs must be merged with code by the freeze. #### What happens if these deadlines are missed? @@ -198,8 +202,6 @@ and to prevent any last minute surprises. Merge requests should still be complete, following the [definition of done][done]. -#### Feature merge requests - If a merge request is not ready, but the developers and Product Manager responsible for the feature think it is essential that it is in the release, they can [ask for an exception](#asking-for-an-exception) in advance. This is @@ -214,34 +216,17 @@ information, see [Automatic CE->EE merge][automatic_ce_ee_merge] and [Guidelines for implementing Enterprise Edition features][ee_features]. -#### Documentation merge requests - -Documentation is part of the product and must be shipped with the feature. - -The single exception for the feature freeze is documentation, and it can -be left to be **merged up to the 14th** if: - -* There is a follow-up issue to add documentation. -* It is assigned to the developer writing documentation for this feature, and they - are aware of it. -* It is in the correct milestone, with the labels ~Documentation, ~Deliverable, -~missed-deliverable, and "pick into X.Y" applied. -* It must be reviewed and approved by a technical writer. - -For more information read the process for -[documentation shipped late](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late). - ### After the 7th Once the stable branch is frozen, the only MRs that can be cherry-picked into the stable branch are: * Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section. -* Fixes for security issues -* Fixes or improvements to automated QA scenarios -* [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release -* New or updated translations (as long as they do not touch application code) -* Changes that are behind a feature flag and have the ~"feature flag" label +* Fixes for security issues. +* Fixes or improvements to automated QA scenarios. +* [Documentation improvements](https://docs.gitlab.com/ee/development/documentation/workflow.html) for feature changes made in the same release, though initial docs for these features should have already been merged by the freeze, as required. +* New or updated translations (as long as they do not touch application code). +* Changes that are behind a feature flag and have the ~"feature flag" label. During the feature freeze all merge requests that are meant to go into the upcoming release should have the correct milestone assigned _and_ the diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index 2e192c958ba..11aec312368 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -2,7 +2,7 @@ import Service from '../services'; import * as types from './mutation_types'; import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; let eTagPoll; @@ -19,9 +19,17 @@ export function startPolling({ commit }, endpoint) { commit(types.SET_EXTERNAL_URL, data.external_url); commit(types.SET_LOADING, false); }, - errorCallback: () => { + errorCallback: response => { + let errorMessage = ''; + if (response && response.data && response.data.message) { + errorMessage = response.data.message; + } commit(types.SET_LOADING, false); - createFlash(__('Failed to load errors from Sentry')); + createFlash( + sprintf(__(`Failed to load errors from Sentry. Error message: %{errorMessage}`), { + errorMessage, + }), + ); }, }); diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index a63571edcea..8d3f6d902f8 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -90,8 +90,15 @@ export default { this.fetchNotes(); } }, + allDiscussions() { + if (this.discussonsCount) { + this.discussonsCount.textContent = this.allDiscussions.length; + } + }, }, created() { + this.discussonsCount = document.querySelector('.js-discussions-count'); + this.setNotesData(this.notesData); this.setNoteableData(this.noteableData); this.setUserData(this.userData); diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 39cd891c111..636308c5401 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -221,7 +221,7 @@ export default class UserTabs { const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); - const calendarHint = __('Issues, merge requests, pushes and comments.'); + const calendarHint = __('Issues, merge requests, pushes, and comments.'); $calendarWrap.html(CALENDAR_TEMPLATE); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c1f2f5f8c6a..fa424532879 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -52,6 +52,11 @@ word-break: break-all; } +.text-underline, +.text-underline:hover { + text-decoration: underline; +} + .hint { font-style: italic; color: $gl-gray-400; } .light { color: $gl-text-color; } diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 94002095739..0227af2c266 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -37,6 +37,14 @@ class Profiles::PreferencesController < Profiles::ApplicationController end def preferences_param_names - [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id, :first_day_of_week] + [ + :color_scheme_id, + :layout, + :dashboard, + :project_view, + :theme_id, + :first_day_of_week, + :preferred_language + ] end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 15248d2d08f..b9c52618d4b 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -104,7 +104,6 @@ class ProfilesController < Profiles::ApplicationController :username, :website_url, :organization, - :preferred_language, :private_profile, :include_private_contributions, status: [:emoji, :message] diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb index 5290313585f..8de3276184d 100644 --- a/app/finders/concerns/finder_methods.rb +++ b/app/finders/concerns/finder_methods.rb @@ -3,13 +3,13 @@ module FinderMethods # rubocop: disable CodeReuse/ActiveRecord def find_by!(*args) - raise_not_found_unless_authorized execute.find_by!(*args) + raise_not_found_unless_authorized execute.reorder(nil).find_by!(*args) end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def find_by(*args) - if_authorized execute.find_by(*args) + if_authorized execute.reorder(nil).find_by(*args) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 23af2e0521c..5870f158690 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -94,6 +94,7 @@ class IssuableFinder items = by_scope(items) items = by_created_at(items) items = by_updated_at(items) + items = by_closed_at(items) items = by_state(items) items = by_group(items) items = by_assignee(items) @@ -353,6 +354,13 @@ class IssuableFinder items end + def by_closed_at(items) + items = items.closed_after(params[:closed_after]) if params[:closed_after].present? + items = items.closed_before(params[:closed_before]) if params[:closed_before].present? + + items + end + # rubocop: disable CodeReuse/ActiveRecord def by_state(items) case params[:state].to_s diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index fd1b46ba860..b98d8bd1fff 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -9,7 +9,30 @@ module Resolvers argument :iids, [GraphQL::ID_TYPE], required: false, description: 'The list of IIDs of issues, e.g., [1, 2]' - + argument :state, Types::IssuableStateEnum, + required: false, + description: "Current state of Issue" + argument :label_name, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: "Labels applied to the Issue" + argument :created_before, Types::TimeType, + required: false, + description: "Issues created before this date" + argument :created_after, Types::TimeType, + required: false, + description: "Issues created after this date" + argument :updated_before, Types::TimeType, + required: false, + description: "Issues updated before this date" + argument :updated_after, Types::TimeType, + required: false, + description: "Issues updated after this date" + argument :closed_before, Types::TimeType, + required: false, + description: "Issues closed before this date" + argument :closed_after, Types::TimeType, + required: false, + description: "Issues closed after this date" argument :search, GraphQL::STRING_TYPE, required: false argument :sort, Types::Sort, diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb new file mode 100644 index 00000000000..f2f6d6c6cab --- /dev/null +++ b/app/graphql/types/issuable_state_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class IssuableStateEnum < BaseEnum + graphql_name 'IssuableState' + description 'State of a GitLab issue or merge request' + + value 'opened' + value 'closed' + value 'locked' + end +end diff --git a/app/graphql/types/issue_state_enum.rb b/app/graphql/types/issue_state_enum.rb new file mode 100644 index 00000000000..6521407fc9d --- /dev/null +++ b/app/graphql/types/issue_state_enum.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class IssueStateEnum < IssuableStateEnum + graphql_name 'IssueState' + description 'State of a GitLab issue' + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index a8f2f7914a8..87f6b1f8278 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -11,7 +11,7 @@ module Types field :iid, GraphQL::ID_TYPE, null: false field :title, GraphQL::STRING_TYPE, null: false field :description, GraphQL::STRING_TYPE, null: true - field :state, GraphQL::STRING_TYPE, null: false + field :state, IssueStateEnum, null: false field :author, Types::UserType, null: false, diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb new file mode 100644 index 00000000000..92f52726ab3 --- /dev/null +++ b/app/graphql/types/merge_request_state_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class MergeRequestStateEnum < IssuableStateEnum + graphql_name 'MergeRequestState' + description 'State of a GitLab merge request' + + value 'merged' + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 7e63d4022b1..7827b6e3717 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -12,7 +12,7 @@ module Types field :iid, GraphQL::ID_TYPE, null: false field :title, GraphQL::STRING_TYPE, null: false field :description, GraphQL::STRING_TYPE, null: true - field :state, GraphQL::STRING_TYPE, null: true + field :state, MergeRequestStateEnum, null: false field :created_at, Types::TimeType, null: false field :updated_at, Types::TimeType, null: false field :source_project, Types::ProjectType, null: true diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 1a471034972..af28e6fcb93 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -200,7 +200,7 @@ module IssuablesHelper author_output end - output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) + output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip prepend-left-4', title: _('1st contribution!')) output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index bc1742e8167..eed529f93db 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -62,6 +62,10 @@ module PreferencesHelper Gitlab::ColorSchemes.for_user(current_user).css_class end + def language_choices + Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] } + end + private # Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f0ae516a2f8..eb15347b4e1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -47,6 +47,8 @@ module Ci has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' + has_one :chat_data, class_name: 'Ci::PipelineChatData' + accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :id, to: :project, prefix: true diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb new file mode 100644 index 00000000000..8d37500fec5 --- /dev/null +++ b/app/models/ci/pipeline_chat_data.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + class PipelineChatData < ActiveRecord::Base + self.table_name = 'ci_pipeline_chat_data' + + belongs_to :chat_name + + validates :pipeline_id, presence: true + validates :chat_name_id, presence: true + validates :response_url, presence: true + end +end diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 2994aaae4aa..4be4fdb1ff2 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -22,6 +22,7 @@ module Ci schedule: 4, api: 5, external: 6, + chat: 8, merge_request: 10 } end diff --git a/app/models/concerns/closed_at_filterable.rb b/app/models/concerns/closed_at_filterable.rb new file mode 100644 index 00000000000..239c2e47611 --- /dev/null +++ b/app/models/concerns/closed_at_filterable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ClosedAtFilterable + extend ActiveSupport::Concern + + included do + scope :closed_before, ->(date) { where(scoped_table[:closed_at].lteq(date)) } + scope :closed_after, ->(date) { where(scoped_table[:closed_at].gteq(date)) } + + def self.scoped_table + arel_table.alias(table_name) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0a77fbeba08..429a63f83cc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -23,6 +23,7 @@ module Issuable include Sortable include CreatedAtFilterable include UpdatedAtFilterable + include ClosedAtFilterable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 31084c54bdc..57283a78ea9 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -58,7 +58,7 @@ module ErrorTracking def list_sentry_issues(opts = {}) with_reactive_cache('list_issues', opts.stringify_keys) do |result| - { issues: result } + result end end @@ -69,8 +69,10 @@ module ErrorTracking def calculate_reactive_cache(request, opts) case request when 'list_issues' - sentry_client.list_issues(**opts.symbolize_keys) + { issues: sentry_client.list_issues(**opts.symbolize_keys) } end + rescue Sentry::Client::Error => e + { error: e.message } end # http://HOST/api/0/projects/ORG/PROJECT diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 6c82e088231..6a454070fe2 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -22,6 +22,10 @@ class SlackSlashCommandsService < SlashCommandsService end end + def chat_responder + ::Gitlab::Chat::Responder::Slack + end + private def format(text) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 354e53a367c..c4f69175de3 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -36,6 +36,7 @@ module Ci project: project, current_user: current_user, push_options: params[:push_options], + chat_data: params[:chat_data], **extra_options(options)) sequence = Gitlab::Ci::Pipeline::Chain::Sequence diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb index 4cc35cfa4a8..a6c6bec9598 100644 --- a/app/services/error_tracking/list_issues_service.rb +++ b/app/services/error_tracking/list_issues_service.rb @@ -6,15 +6,19 @@ module ErrorTracking DEFAULT_LIMIT = 20 def execute - return error('not enabled') unless enabled? - return error('access denied') unless can_read? + return error('Error Tracking is not enabled') unless enabled? + return error('Access denied', :unauthorized) unless can_read? result = project_error_tracking_setting .list_sentry_issues(issue_status: issue_status, limit: limit) # our results are not yet ready unless result - return error('not ready', :no_content) + return error('Not ready. Try again later', :no_content) + end + + if result[:error].present? + return error(result[:error], :bad_request) end success(issues: result[:issues]) diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 1a9aca1f6bf..bfe1c3ddf33 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -73,6 +73,12 @@ = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' .col-lg-8 .form-group + = f.label :preferred_language, class: 'label-bold' do + = _('Language') + = f.select :preferred_language, language_choices, {}, class: 'select2' + .form-text.text-muted + = s_('Preferences|This feature is experimental and translations are not complete yet') + .form-group = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 753316b27e2..4d3d92d09c0 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -95,9 +95,6 @@ = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } }, control_class: 'select2 input-lg' - = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, - { help: s_("Profiles|This feature is experimental and translations are not complete yet") }, - control_class: 'select2 input-lg' = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username") = f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename") = f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username") diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 4917f4b8903..42b6aaa2634 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -5,7 +5,7 @@ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' -%section.js-vue-notes-event +%section.issuable-discussion.js-vue-notes-event #js-vue-notes{ data: { notes_data: notes_data(@issue).to_json, noteable_data: serialize_issuable(@issue), noteable_type: 'Issue', diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 653b7d4c6f3..3a674da6e87 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -14,9 +14,12 @@ .detail-page-header-body .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) } = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none') - %span.d-none.d-sm-block + .d-none.d-sm-block - if @issue.moved? - = _("Closed (moved)") + - moved_link_start = "<a href=\"#{issue_path(@issue.moved_to)}\" class=\"text-white text-underline\">".html_safe + - moved_link_end = '</a>'.html_safe + = s_('IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})').html_safe % {moved_link_start: moved_link_start, + moved_link_end: moved_link_end} - else = _("Closed") .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } @@ -88,7 +91,6 @@ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } = render 'new_branch' unless @issue.confidential? - %section.issuable-discussion - = render 'projects/issues/discussion' + = render_if_exists 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 69a47faabed..9c2efd6aa35 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -23,7 +23,7 @@ - if @pipeline.queued_duration = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" - .well-segment + .well-segment.qa-pipeline-badges .icon-container = sprite_icon('flag') - if @pipeline.latest? diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml index 422be28737c..755fd3a17d3 100644 --- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml +++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml @@ -1,5 +1,5 @@ - if show_auto_devops_implicitly_enabled_banner?(project, current_user) - .auto-devops-implicitly-enabled-banner.alert.alert-warning + .qa-auto-devops-banner.auto-devops-implicitly-enabled-banner.alert.alert-warning - more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link' - auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link } = auto_devops_message.html_safe diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 410411b1294..d0fc130b04f 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -101,6 +101,7 @@ - authorized_projects - background_migration +- chat_notification - create_gpg_signature - delete_merged_branches - delete_user diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index ae853ec9316..adc38226405 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -30,5 +30,6 @@ class BuildFinishedWorker # We execute these async as these are independent operations. BuildHooksWorker.perform_async(build.id) ArchiveTraceWorker.perform_async(build.id) + ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat? end end diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb new file mode 100644 index 00000000000..25a306e94d8 --- /dev/null +++ b/app/workers/chat_notification_worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class ChatNotificationWorker + include ApplicationWorker + + RESCHEDULE_INTERVAL = 2.seconds + + # rubocop: disable CodeReuse/ActiveRecord + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + send_response(build) + end + rescue Gitlab::Chat::Output::MissingBuildSectionError + # The creation of traces and sections appears to be eventually consistent. + # As a result it's possible for us to run the above code before the trace + # sections are present. To better handle such cases we'll just reschedule + # the job instead of producing an error. + self.class.perform_in(RESCHEDULE_INTERVAL, build_id) + end + # rubocop: enable CodeReuse/ActiveRecord + + def send_response(build) + Gitlab::Chat::Responder.responder_for(build).try do |responder| + if build.success? + output = Gitlab::Chat::Output.new(build) + + responder.success(output.to_s) + else + responder.failure + end + end + end +end diff --git a/.babelrc.js b/babel.config.js index 1b05a67354e..e3de8ef2d83 100644 --- a/.babelrc.js +++ b/babel.config.js @@ -1,3 +1,5 @@ +/* eslint-disable import/no-commonjs, filenames/match-regex */ + const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null; const presets = [ diff --git a/changelogs/unreleased/35638-move-language-setting-to-preferences.yml b/changelogs/unreleased/35638-move-language-setting-to-preferences.yml new file mode 100644 index 00000000000..d8658218676 --- /dev/null +++ b/changelogs/unreleased/35638-move-language-setting-to-preferences.yml @@ -0,0 +1,5 @@ +--- +title: Move language setting to preferences +merge_request: 25427 +author: Fabian Schneider @fabsrc +type: changed diff --git a/changelogs/unreleased/39010-add-left-margin-to-1st-time-contributor-badge.yml b/changelogs/unreleased/39010-add-left-margin-to-1st-time-contributor-badge.yml new file mode 100644 index 00000000000..758b97deb3b --- /dev/null +++ b/changelogs/unreleased/39010-add-left-margin-to-1st-time-contributor-badge.yml @@ -0,0 +1,5 @@ +--- +title: Add left margin to 1st time contributor badge +merge_request: 25216 +author: Gokhan Apaydin +type: fixed diff --git a/changelogs/unreleased/56492-implement-new-arguments-state-closed_before-and-closed_after-for-issuesresolver-in-graphql.yml b/changelogs/unreleased/56492-implement-new-arguments-state-closed_before-and-closed_after-for-issuesresolver-in-graphql.yml new file mode 100644 index 00000000000..9b7aed82d49 --- /dev/null +++ b/changelogs/unreleased/56492-implement-new-arguments-state-closed_before-and-closed_after-for-issuesresolver-in-graphql.yml @@ -0,0 +1,5 @@ +--- +title: "Implement new arguments `state`, `closed_before` and `closed_after` for `IssuesResolver` in GraphQL" +merge_request: 24910 +author: +type: changed diff --git a/changelogs/unreleased/56871-list-issues-error.yml b/changelogs/unreleased/56871-list-issues-error.yml new file mode 100644 index 00000000000..af5585c6b5d --- /dev/null +++ b/changelogs/unreleased/56871-list-issues-error.yml @@ -0,0 +1,5 @@ +--- +title: Display error message when API call to list Sentry issues fails +merge_request: 24936 +author: +type: fixed diff --git a/changelogs/unreleased/57784-make-closed-duplicate-and-closed-moved-button-a-link-to-target.yml b/changelogs/unreleased/57784-make-closed-duplicate-and-closed-moved-button-a-link-to-target.yml new file mode 100644 index 00000000000..2775d9f4e36 --- /dev/null +++ b/changelogs/unreleased/57784-make-closed-duplicate-and-closed-moved-button-a-link-to-target.yml @@ -0,0 +1,5 @@ +--- +title: Add Link from Closed (moved) Issues to Moved Issue +merge_request: 25300 +author: +type: added diff --git a/changelogs/unreleased/move_chatops_to_core.yml b/changelogs/unreleased/move_chatops_to_core.yml new file mode 100644 index 00000000000..7a75efedfa8 --- /dev/null +++ b/changelogs/unreleased/move_chatops_to_core.yml @@ -0,0 +1,5 @@ +--- +title: Move ChatOps to Core +merge_request: 24780 +author: +type: changed diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 1e094c03171..90cd787d5ac 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -86,3 +86,4 @@ - [delete_stored_files, 1] - [remote_mirror_notification, 2] - [import_issues_csv, 2] + - [chat_notification, 2] diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index 2b09536f42a..0cf478b4f89 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -3,34 +3,34 @@ docs_paths_to_review = helper.changes_by_category[:docs] unless docs_paths_to_review.empty? - message 'This merge request adds or changes files that require a ' \ - 'review from the Docs team.' + message 'This merge request adds or changes files that require a review ' \ + 'from the Technical Writing team whether before or after merging.' markdown(<<~MARKDOWN) -## Docs review +## Documentation review -The following files require a review from the Documentation team: +The following files will require a review from the [Technical Writing](https://about.gitlab.com/handbook/product/technical-writing/) team: * #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")} -When your content is ready for review, assign the MR to a technical writer -according to the [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) -in the table below. If necessary, mention them in a comment explaining what needs -to be reviewed. +Per the [documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html), the review may occur prior to merging this MR **or** after a maintainer has agreed to review and merge this MR without a tech writer review. (Meanwhile, you are welcome to involve a technical writer at any time if you have questions about writing or updating the documentation. GitLabbers are also welcome to use the [#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.) + +The technical writer who, by default, will review this content or collaborate as needed is dependent on the [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages) to which the content applies: | Tech writer | Stage(s) | | ------------ | ------------------------------------------------------------ | | `@marcia` | ~Create ~Release + ~"development guidelines" | -| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitor ~Package ~Secure | +| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitor ~Package ~Secure | | `@eread` | ~Manage ~Configure ~Geo ~Verify | | `@mikelewis` | ~Plan | -You are welcome to mention them sooner if you have questions about writing or -updating the documentation. GitLabbers are also welcome to use the -[#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack. +**To request a review prior to merging** + +- Assign the MR to the applicable technical writer, above. If you are not sure which category the change falls within, or the change is not part of one of these categories, mention one of the usernames above. + +**To request a review to commence after merging** -If you are not sure which category the change falls within, or the change is not -part of one of these categories, mention one of the usernames above. +- Create an issue for a doc review [using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review) and assign it to the applicable technicial writer from the table. MARKDOWN unless gitlab.mr_labels.include?('Documentation') diff --git a/doc/README.md b/doc/README.md index f87ff925e94..14e8d2edb90 100644 --- a/doc/README.md +++ b/doc/README.md @@ -284,6 +284,7 @@ The following documentation relates to the DevOps **Configure** stage: | [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. | | [Easy creation of Kubernetes<br/>clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) | Use Google Kubernetes Engine and GitLab. | | [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. | +| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. | | [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | | [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | | [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. | diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index bf5d064d79d..55d85ad86e6 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -359,6 +359,12 @@ following section assumes you are using Omnibus GitLab Enterprise Edition. For the Omnibus Community Edition and installations from source, follow the [Redis HA source install](redis_source.md) guide. +NOTE: **Note:** If you are using an external Redis Sentinel instance, be sure +to exclude the `requirepass` parameter from the Sentinel +configuration. This parameter will cause clients to report `NOAUTH +Authentication required.`. [Redis Sentinel 3.2.x does not support +password authentication](https://github.com/antirez/redis/issues/3279). + Now that the Redis servers are all set up, let's configure the Sentinel servers. diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md index 05873e01a08..658b2f55d30 100644 --- a/doc/administration/incoming_email.md +++ b/doc/administration/incoming_email.md @@ -95,244 +95,249 @@ for a real-world example of this exploit. ### Omnibus package installations -1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the - feature and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature + and fill in the details for your specific IMAP server and email account (see [examples](#config-examples) below). - Configuration for Postfix mail server, assumes mailbox - incoming@gitlab.example.com +1. Reconfigure GitLab for the changes to take effect: - ```ruby - gitlab_rails['incoming_email_enabled'] = true + ```sh + sudo gitlab-ctl reconfigure + sudo gitlab-ctl restart + ``` - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" +1. Verify that everything is configured correctly: - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - gitlab_rails['incoming_email_email'] = "incoming" - # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" + ```sh + sudo gitlab-rake gitlab:incoming_email:check + ``` - # IMAP server host - gitlab_rails['incoming_email_host'] = "gitlab.example.com" - # IMAP server port - gitlab_rails['incoming_email_port'] = 143 - # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = false - # Whether the IMAP server uses StartTLS - gitlab_rails['incoming_email_start_tls'] = false +Reply by email should now be working. - # The mailbox where incoming mail will end up. Usually "inbox". - gitlab_rails['incoming_email_mailbox_name'] = "inbox" - # The IDLE command timeout. - gitlab_rails['incoming_email_idle_timeout'] = 60 +### Installations from source + +1. Go to the GitLab installation directory: + + ```sh + cd /home/git/gitlab ``` - Configuration for Gmail / Google Apps, assumes mailbox - gitlab-incoming@gmail.com +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature + and fill in the details for your specific IMAP server and email account (see [examples](#config-examples) below). - ```ruby - gitlab_rails['incoming_email_enabled'] = true +1. Enable `mail_room` in the init script at `/etc/default/gitlab`: + + ```sh + sudo mkdir -p /etc/default + echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab + ``` + +1. Restart GitLab: + + ```sh + sudo service gitlab restart + ``` + +1. Verify that everything is configured correctly: + + ```sh + sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production + ``` + +Reply by email should now be working. + +### Config examples + +#### Postfix + +Example configuration for Postfix mail server. Assumes mailbox incoming@gitlab.example.com. + +Example for Omnibus installs: + +```ruby +gitlab_rails['incoming_email_enabled'] = true + +# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. +# The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). +gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" + +# Email account username +# With third party providers, this is usually the full email address. +# With self-hosted email servers, this is usually the user part of the email address. +gitlab_rails['incoming_email_email'] = "incoming" +# Email account password +gitlab_rails['incoming_email_password'] = "[REDACTED]" + +# IMAP server host +gitlab_rails['incoming_email_host'] = "gitlab.example.com" +# IMAP server port +gitlab_rails['incoming_email_port'] = 143 +# Whether the IMAP server uses SSL +gitlab_rails['incoming_email_ssl'] = false +# Whether the IMAP server uses StartTLS +gitlab_rails['incoming_email_start_tls'] = false + +# The mailbox where incoming mail will end up. Usually "inbox". +gitlab_rails['incoming_email_mailbox_name'] = "inbox" +# The IDLE command timeout. +gitlab_rails['incoming_email_idle_timeout'] = 60 +``` + +Example for source installs: + +```yaml +incoming_email: + enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" + address: "incoming+%{key}@gitlab.example.com" # Email account username # With third party providers, this is usually the full email address. # With self-hosted email servers, this is usually the user part of the email address. - gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" + user: "incoming" # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" + password: "[REDACTED]" # IMAP server host - gitlab_rails['incoming_email_host'] = "imap.gmail.com" + host: "gitlab.example.com" # IMAP server port - gitlab_rails['incoming_email_port'] = 993 + port: 143 # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = true + ssl: false # Whether the IMAP server uses StartTLS - gitlab_rails['incoming_email_start_tls'] = false + start_tls: false # The mailbox where incoming mail will end up. Usually "inbox". - gitlab_rails['incoming_email_mailbox_name'] = "inbox" + mailbox: "inbox" # The IDLE command timeout. - gitlab_rails['incoming_email_idle_timeout'] = 60 - ``` + idle_timeout: 60 +``` + +#### Gmail + +Example configuration for Gmail/G Suite. Assumes mailbox gitlab-incoming@gmail.com. + +Example for Omnibus installs: + +```ruby +gitlab_rails['incoming_email_enabled'] = true - Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes the - catch-all mailbox incoming@exchange.example.com +# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. +# The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). +gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" - ```ruby - gitlab_rails['incoming_email_enabled'] = true +# Email account username +# With third party providers, this is usually the full email address. +# With self-hosted email servers, this is usually the user part of the email address. +gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" +# Email account password +gitlab_rails['incoming_email_password'] = "[REDACTED]" + +# IMAP server host +gitlab_rails['incoming_email_host'] = "imap.gmail.com" +# IMAP server port +gitlab_rails['incoming_email_port'] = 993 +# Whether the IMAP server uses SSL +gitlab_rails['incoming_email_ssl'] = true +# Whether the IMAP server uses StartTLS +gitlab_rails['incoming_email_start_tls'] = false + +# The mailbox where incoming mail will end up. Usually "inbox". +gitlab_rails['incoming_email_mailbox_name'] = "inbox" +# The IDLE command timeout. +gitlab_rails['incoming_email_idle_timeout'] = 60 +``` + +Example for source installs: + +```yaml +incoming_email: + enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - # Exchange does not support sub-addressing, so a catch-all mailbox must be used. - gitlab_rails['incoming_email_address'] = "incoming-%{key}@exchange.example.com" + address: "gitlab-incoming+%{key}@gmail.com" # Email account username - # Typically this is the userPrincipalName (UPN) - gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com" + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" + password: "[REDACTED]" # IMAP server host - gitlab_rails['incoming_email_host'] = "exchange.example.com" + host: "imap.gmail.com" # IMAP server port - gitlab_rails['incoming_email_port'] = 993 + port: 993 # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = true - ``` - -1. Reconfigure GitLab for the changes to take effect: + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false - ```sh - sudo gitlab-ctl reconfigure - sudo gitlab-ctl restart - ``` + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 +``` -1. Verify that everything is configured correctly: +#### MS Exchange - ```sh - sudo gitlab-rake gitlab:incoming_email:check - ``` +Example configuration for Microsoft Exchange mail server with IMAP enabled. Assumes the +catch-all mailbox incoming@exchange.example.com. -1. Reply by email should now be working. +Example for Omnibus installs: -### Installations from source +```ruby +gitlab_rails['incoming_email_enabled'] = true -1. Go to the GitLab installation directory: +# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. +# The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). +# Exchange does not support sub-addressing, so a catch-all mailbox must be used. +gitlab_rails['incoming_email_address'] = "incoming-%{key}@exchange.example.com" - ```sh - cd /home/git/gitlab - ``` +# Email account username +# Typically this is the userPrincipalName (UPN) +gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com" +# Email account password +gitlab_rails['incoming_email_password'] = "[REDACTED]" -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature - and fill in the details for your specific IMAP server and email account: +# IMAP server host +gitlab_rails['incoming_email_host'] = "exchange.example.com" +# IMAP server port +gitlab_rails['incoming_email_port'] = 993 +# Whether the IMAP server uses SSL +gitlab_rails['incoming_email_ssl'] = true +``` - ```sh - sudo editor config/gitlab.yml - ``` +Example for source installs: - Configuration for Postfix mail server, assumes mailbox - incoming@gitlab.example.com - - ```yaml - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - address: "incoming+%{key}@gitlab.example.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - user: "incoming" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "gitlab.example.com" - # IMAP server port - port: 143 - # Whether the IMAP server uses SSL - ssl: false - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` +```yaml +incoming_email: + enabled: true - Configuration for Gmail / Google Apps, assumes mailbox - gitlab-incoming@gmail.com - - ```yaml - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - address: "gitlab-incoming+%{key}@gmail.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - user: "gitlab-incoming@gmail.com" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "imap.gmail.com" - # IMAP server port - port: 993 - # Whether the IMAP server uses SSL - ssl: true - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - - Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes the - catch-all mailbox incoming@exchange.example.com - - ```yaml - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - # Exchange does not support sub-addressing, so a catch-all mailbox must be used. - address: "incoming-%{key}@exchange.example.com" - - # Email account username - # Typically this is the userPrincipalName (UPN) - user: "incoming@ad-domain.example.com" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "exchange.example.com" - # IMAP server port - port: 993 - # Whether the IMAP server uses SSL - ssl: true - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - -1. Enable `mail_room` in the init script at `/etc/default/gitlab`: - - ```sh - sudo mkdir -p /etc/default - echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab - ``` - -1. Restart GitLab: - - ```sh - sudo service gitlab restart - ``` + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + # Exchange does not support sub-addressing, so a catch-all mailbox must be used. + address: "incoming-%{key}@exchange.example.com" -1. Verify that everything is configured correctly: + # Email account username + # Typically this is the userPrincipalName (UPN) + user: "incoming@ad-domain.example.com" + # Email account password + password: "[REDACTED]" - ```sh - sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production - ``` + # IMAP server host + host: "exchange.example.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false -1. Reply by email should now be working. + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 +``` diff --git a/doc/administration/index.md b/doc/administration/index.md index fff4538a7c9..a27c11c4f64 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -51,7 +51,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Third party offers](../user/admin_area/settings/third_party_offers.md) - [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards. - [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages. -- [Merge request diffs](merge_request_diffs.md): Configure the diffs shown on merge requests +- [Merge request diffs storage](merge_request_diffs.md): Configure merge requests diffs external storage. - [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI. #### Customizing GitLab's appearance diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md index 94620c3d3a0..c34a9519ace 100644 --- a/doc/administration/merge_request_diffs.md +++ b/doc/administration/merge_request_diffs.md @@ -1,7 +1,6 @@ -# Merge request diffs administration +# Merge request diffs storage **[CORE ONLY]** -> **Notes:** -> - External merge request diffs introduced in GitLab 11.8 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/52568) in GitLab 11.8. Merge request diffs are size-limited copies of diffs associated with merge requests. When viewing a merge request, diffs are sourced from these copies @@ -16,9 +15,7 @@ large, in which case, switching to external storage is recommended. Merge request diffs can be stored on disk, or in object storage. In general, it is better to store the diffs in the database than on disk. -To enable external storage of merge request diffs: - ---- +To enable external storage of merge request diffs, follow the instructions below. **In Omnibus installations:** @@ -29,17 +26,15 @@ To enable external storage of merge request diffs: ``` 1. _The external diffs will be stored in in - `/var/opt/gitlab/gitlab-rails/shared/external-diffs`._ To change the path, - for example to `/mnt/storage/external-diffs`, edit `/etc/gitlab/gitlab.rb` + `/var/opt/gitlab/gitlab-rails/shared/external-diffs`._ To change the path, + for example, to `/mnt/storage/external-diffs`, edit `/etc/gitlab/gitlab.rb` and add the following line: ```ruby gitlab_rails['external_diffs_storage_path'] = "/mnt/storage/external-diffs" ``` -1. Save the file and [reconfigure GitLab][] for the changes to take effect. - ---- +1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. **In installations from source:** @@ -52,7 +47,7 @@ To enable external storage of merge request diffs: ``` 1. _The external diffs will be stored in - `/home/git/gitlab/shared/external-diffs`._ To change the path, for example + `/home/git/gitlab/shared/external-diffs`._ To change the path, for example, to `/mnt/storage/external-diffs`, edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: @@ -62,18 +57,18 @@ To enable external storage of merge request diffs: storage_path: /mnt/storage/external-diffs ``` -1. Save the file and [restart GitLab][] for the changes to take effect. +1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect. ### Using object storage -Instead of storing the external diffs on disk, we recommended you use an object +Instead of storing the external diffs on disk, we recommended the use of an object store like AWS S3 instead. This configuration relies on valid AWS credentials to be configured already. ### Object Storage Settings For source installations, these settings are nested under `external_diffs:` and -then `object_store:`. On omnibus installs, they are prefixed by +then `object_store:`. On Omnibus installations, they are prefixed by `external_diffs_object_store_`. | Setting | Description | Default | @@ -118,7 +113,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a } ``` - NOTE: if you are using AWS IAM profiles, be sure to omit the + Note that, if you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs. For example: ```ruby @@ -129,9 +124,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a } ``` -1. Save the file and [reconfigure GitLab][] for the changes to take effect. - ---- +1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. **In installations from source:** @@ -151,4 +144,4 @@ The connection settings match those provided by [Fog](https://github.com/fog), a region: eu-central-1 ``` -1. Save the file and [restart GitLab][] for the changes to take effect. +1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect. diff --git a/doc/api/README.md b/doc/api/README.md index 89069fe60e1..48d9ad8f30d 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -134,7 +134,7 @@ Endpoints are available for: ## Road to GraphQL Going forward, we will start on moving to -[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of +[GraphQL](graphql/index.md) and deprecate the use of controller-specific endpoints. GraphQL has a number of benefits: 1. We avoid having to maintain two different APIs. diff --git a/doc/api/services.md b/doc/api/services.md index 5d5aa3e5b3e..9275a1ccda8 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -522,6 +522,7 @@ Parameters: | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `username` | string | yes | The username of the user created to be used with GitLab/JIRA. | | `password` | string | yes | The password of the user created to be used with GitLab/JIRA. | +| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service diff --git a/doc/ci/README.md b/doc/ci/README.md index e44db230a7f..4c3c9920b93 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -93,6 +93,7 @@ use of advanced features: | [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. | | [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. | | [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. | +| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | | [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | ### GitLab Pages diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md new file mode 100644 index 00000000000..6ad1df7bb2a --- /dev/null +++ b/doc/ci/chatops/README.md @@ -0,0 +1,61 @@ +# GitLab ChatOps + +> **Notes:** +> +> * [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. +> +> * ChatOps is currently in alpha, with some important features missing like access control. + +GitLab ChatOps provides a method to interact with CI/CD jobs through chat services like Slack. Many organizations' discussion, collaboration, and troubleshooting is taking place in chat services these days, and having a method to run CI/CD jobs with output posted back to the channel can significantly augment a team's workflow. + +## How it works + +GitLab ChatOps is built upon two existing features, [GitLab CI/CD](../README.md) and [Slack Slash Commmands](../../user/project/integrations/slack_slash_commands.md). + +A new `run` action has been added to the [slash commands](../../integration/slash_commands.md), which takes two arguments: a `<job name>` to execute and the `<job arguments>`. When executed, ChatOps will look up the specified job name and attempt to match it to a corresponding job in [.gitlab-ci.yml](../yaml/README.md). If a matching job is found on `master`, a pipeline containing just that job is scheduled. Two additional [CI/CD variables](../variables/README.html#predefined-variables-environment-variables) are passed to the job: `CHAT_INPUT` contains any additional arguments, and `CHAT_CHANNEL` is set to the name of channel the action was triggered in. + +After the job has finished, its output is sent back to Slack provided it has completed within 30 minutes. If a job takes more than 30 minutes to run it must use the Slack API to manually send data back to a channel. + +[Developer access and above](../../user/permissions.html#project-members-permissions) is required to use the `run` command. If a job should not be able to be triggered from chat, it can be set to `except: [chat]`. + +## Creating a ChatOps CI job + +Since ChatOps is built upon GitLab CI/CD, the job has all the same features and functions available. There a few best practices to consider however when creating ChatOps jobs: + +* It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline. +* If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started. +* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](https://docs.gitlab.com/ce/ci/variables/README.html#priority-of-variables). + +### Controlling the ChatOps reply + +For jobs with a single command, its output is automatically sent back to the channel as a reply. For example the chat reply of the following job is simply `Hello World.` + +```yaml +hello-world: + stage: chatops + only: [chat] + script: + - echo "Hello World." +``` + +Jobs that contain multiple commands, or have a `before_script`, include additional content in the chat reply. In these cases both the commands and their output are included, with the commands wrapped in ANSI colors codes. + +To selectively reply with the output of one command, its output must be bounded by the `chat_reply` section. For example, the following job will list the files in the current directory. + +```yaml +ls: + stage: chatops + only: [chat] + script: + - echo "This command will not be shown." + - echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K" +``` + +## GitLab ChatOps icon + +Say Hi to our ChatOps bot. +You can find and download the official GitLab ChatOps icon here. + +![GitLab ChatOps bot icon](img/gitlab-chatops-icon-small.png) + +[Download bigger image](img/gitlab-chatops-icon.png) diff --git a/doc/ci/chatops/img/gitlab-chatops-icon-small.png b/doc/ci/chatops/img/gitlab-chatops-icon-small.png Binary files differnew file mode 100644 index 00000000000..71cc5dba5cf --- /dev/null +++ b/doc/ci/chatops/img/gitlab-chatops-icon-small.png diff --git a/doc/ci/chatops/img/gitlab-chatops-icon.png b/doc/ci/chatops/img/gitlab-chatops-icon.png Binary files differnew file mode 100644 index 00000000000..3ba8bd308e3 --- /dev/null +++ b/doc/ci/chatops/img/gitlab-chatops-icon.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 7498617ed2c..ca668ac526f 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -53,6 +53,8 @@ future GitLab releases.** | Variable | GitLab | Runner | Description | |-------------------------------------------|--------|--------|-------------| | **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job | +| **CHAT_INPUT** | 10.6 | all | Additional arguments passed in the [ChatOps](../chatops/README.md) command | +| **CHAT_CHANNEL** | 10.6 | all | Source chat channel which triggered the [ChatOps](../chatops/README.md) command | | **CI** | all | 0.4 | Mark that job is executed in CI environment | | **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. | | **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. | diff --git a/doc/development/documentation/feature-change-workflow.md b/doc/development/documentation/feature-change-workflow.md index b5b325683a3..eb479b85083 100644 --- a/doc/development/documentation/feature-change-workflow.md +++ b/doc/development/documentation/feature-change-workflow.md @@ -2,111 +2,178 @@ description: How to add docs for new or enhanced GitLab features. --- -# Documentation process at GitLab +# Documentation process for feature changes At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process. - **Developers**: Author/update documentation in the same MR as their code, and merge it by the feature freeze for the assigned milestone. Request technical writer -assistance if needed. +assistance if needed. Other developers typically act as reviewers. - **Product Managers** (PMs): In the issue for all new and enhanced features, confirm the documentation requirements, plus the mentioned feature description and use cases, which can be reused in docs. They can bring in a technical -writer for discussion or help, and can be called upon themselves as a doc reviewer. +writer for discussion or collaboration, and can be called upon themselves as a doc reviewer. - **Technical Writers**: Review doc requirements in issues, track issues and MRs that contain docs changes, help with any questions throughout the authoring/editing process, -and review all new and updated docs content after it's merged (unless a pre-merge -review request is made). +work on special projects related to the documentation, and review all new and updated +docs content, whether before or after it is merged. -Beyond this process, any member of the GitLab community can also author documentation +Beyond this process, any member of the GitLab community can also author or request documentation improvements that are not associated with a new or changed feature. See the [Documentation improvement workflow](improvement-workflow.md). ## When documentation is required Documentation must be delivered whenever: -- A new or enhanced feature is shipped that impacts the user/admin experience -- There are changes to the UI or API -- A process, workflow, or previously documented feature is changed -- A feature is deprecated or removed +- A new or enhanced feature is shipped that impacts the user/admin experience. +- There are changes to the UI or API. +- A process, workflow, or previously documented feature is changed. +- A feature is deprecated or removed. + +For example, a UI restyling that offers no difference in functionality may require +documentation updates if screenshots are now needed, or need to be updated. Documentation is not required when a feature is changed on the backend -only and does not directly affect the way that any user or -administrator would interact with GitLab. For example, a UI restyling that offers -no difference in functionality may require documentation updates if screenshots -are now needed, or need to be updated. +only and does not directly affect the way that any user or administrator would +interact with GitLab. NOTE: **Note:** When revamping documentation, if unrelated to the feature change, this should be submitted in its own MR (using the [documentation improvement workflow](improvement-workflow.md)) so that we can ensure the more time-sensitive doc updates are merged with code by the freeze. +## Documentation requirements in feature issues + +Requirements for the documentation of a feature should be included as part of the +issue for planning that feature, in a Documentation section within the issue description. + +This section is provided as part of the Feature Proposal template and should be added +to the issue if it is not already present. + +Anyone can add these details, but the product manager who assigns the issue to a specific release +milestone will ensure these details are present and finalized by the time of that milestone's kickoff. +Developers, technical writers, and others may help further refine this plan at any time. + +### Details to include + +- What concepts and procedures should the docs guide and enable the user to understand or accomplish? +- To this end, what new page(s) are needed, if any? What pages/subsections need updates? Consider user, admin, and API doc changes and additions. +- For any guide or instruction set, should it help address a single use case, or be flexible to address a certain range of use cases? +- Do we need to update a previously recommended workflow? Should we link the new feature from various relevant locations? Consider all ways documentation should be affected. +- Are there any key terms or task descriptions that should be included so that the docs are found in relevant searches? +- Include suggested titles of any pages or subsections, if applicable. + ## Documenting a new or changed feature To follow a consistent workflow every month, documentation changes involve the Product Managers, the developer who shipped the feature, -and the Technical Writing team. Each role is described below. +and the technical writer for the DevOps stage. Each role is described below. + +The Documentation items in the GitLab CE/EE [Feature Proposal issue template](https://gitlab.com/gitlab-org/gitlab-ce/raw/template-improvements-for-documentation/.gitlab/issue_templates/Feature%20proposal.md) +and default merge request template will assist you with following this process. + +### Product Manager role -### 1. Product Manager's role +For issues requiring any new or updated documentation, the Product Manager (PM) +must: -The Product Manager (PM) should confirm or add the following items in the issue: +- Add the `Documentation` label. +- Confirm or add the [documentation requirements](#documentation-requirements). +- Ensure the issue contains any new or updated feature name, overview/description, +and use cases, as required per the [documentation structure and template](structure.md), when applicable. -- New or updated feature name, overview/description, and use cases, all required per the [Documentation structure and template](structure.md). -- The documentation requirements for the developer working on the docs. - - What new page, new subsection of an existing page, or other update to an existing page/subsection is needed. - - Just one page/section/update or multiple (perhaps there's an end user and admin change needing docs, or we need to update a previously recommended workflow, or we want to link the new feature from various places; consider and mention all ways documentation should be affected. - - Suggested title of any page or subsection, if applicable. -- Label the issue with `Documentation` and `docs:P1` in addition to the `Deliverable` label and correct milestone. +Everyone is encouraged to draft the requirements in the issue, but a product manager will +do the following: -Anyone is welcome to draft the items above in the issue, but a product manager must review and update them whenever the issue is assigned a specific milestone. +- When the issue is assigned a release milestone, review and update the Documentation details. +- By the kickoff, finalizie the Documentation details. -### 2. Developer's role +### Developer and maintainer roles + +#### Authoring As a developer, you must ship the documentation with the code of the feature that you are creating or updating. The documentation is an essential part of the product. +Technical writers are happy to help, as requested and planned on an issue-by-issue basis. + +Follow the process below unless otherwise agreed with the product manager and technical writer for a given issue: -- New and edited docs should be included in the MR introducing the code, and planned -in the issue that proposed the feature. However, if the new or changed doc requires -extensive collaboration or conversation, a separate, linked issue can be used for the planning process. +- Include any new and edited docs in the MR introducing the code. +- Use the Documentation requirements confirmed by the Product Manager in the +issue and discuss any further doc plans or ideas as needed. + - If the new or changed doc requires extensive collaboration or conversation, a separate, +linked issue can be used for the planning process. + - We are trying to avoid using a separate MR, so that the docs stay with the code, but the +Technical Writing team is interested in discussing any potential exceptions that may be suggested. - Use the [Documentation guidelines](index.md), as well as other resources linked from there, -including the [Structure and template](structure.md) page, [Style Guide](styleguide.md), and [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). +including the Documentation [Structure and template](structure.md) page, [Style Guide](styleguide.md), and [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). - If you need any help to choose the correct place for a doc, discuss a documentation idea or outline, or request any other help, ping the Technical Writer for the relevant [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages) in your issue or MR, or write within `#docs` on the GitLab Slack. - The docs must be merged with the code **by the feature freeze date**, otherwise -- the feature cannot be included with the release.<!-- TODO: Policy/process for feature-flagged issues --> +the feature cannot be included with the release. A policy for documenting feature-flagged +issues is forthcoming and you are welcome to join the [discussion](https://gitlab.com/gitlab-org/gitlab-ce/issues/56813). -Prior to merge, documentation changes commited by the developer must be reviewed by: -* the person reviewing the code and merging the MR. -* optionally: others involved in the work (such as other devs, the PM, or a technical writer), if requested. +#### Reviews and merging -After merging, documentation changing are reviewed by: -* a technical writer (for clarity, structure, grammar, etc). -* optionally: by the PM (for accuracy and to ensure it's consistent with the vision for how the product will be used). +All reviewers can help ensure accuracy, clarity, completeness, and adherence to the plans in the issue, as well as the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). + +- **Prior to merging**, documentation changes committed by the developer must be reviewed by: + + 1. **The code reviewer** for the MR, to confirm accuracy, clarity, and completeness. + 1. Optionally: Others involved in the work, such as other devs or the PM. + 1. Optionally: The technical writer for the DevOps stage. If not prior to merging, the technical writer will review after the merge. +This helps us ensure that the developer has time to merge good content by the freeze, and that it can be further refined by the release, if needed. + - To decide whether to request this review before the merge, consider the amount of time left before the code freeze, the size of the change, +and your degree of confidence in having users of an RC use your docs as written. + - Pre-merge tech writer reviews should be most common when the code is complete well in advance of the freeze and/or for larger documentation changes. + - You can request a review and if there is not sufficient time to complete it prior to the freeze, +the maintainer can merge the current doc changes (if complete) and create a follow-up doc review issue. + - The technical writer can also help decide what docs to merge before the freeze and whether to work on further changes in a follow up MR. + - **To request a pre-merge technical writer review**, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). + - **To request a post-merge technical writer review**, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review) and link it from the MR that makes the doc change. + 1. **The maintainer** who is assigned to merge the MR, to verify clarity, completeness, and quality, to the best of their ability. + +- Upon merging, if a technical writer review has not been performed and there is not yet a linked issue for a follow-up review, the maintainer should [create an issue using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review), link it from the MR, and +mention the original MR author in the new issue. Alternatively, the mainitainer can ask the MR author to create and link this issue before the MR is merged. + +- After merging, documentation changes are reviewed by: + + 1. The technical writer--**if** their review was not performed prior to the merge. + 2. Optionally: by the PM (for accuracy and to ensure it's consistent with the vision for how the product will be used). Any party can raise the item to the PM for review at any point: the dev, the technical writer, or the PM, who can request/plan a review at the outset. -### 3. Technical Writer's role +### Technical Writer role + +#### Planning + +- The technical writer monitors the documentation needs of issues assigned to the current and next milestone +for their DevOps stage(s), and participates in any needed discussion on docs planning and requirements refinement +with the dev, PM, and others. +- The technical writer will review these requirements again upon the kickoff and provide feedback, as needed. +This is not a blocking review and developers should not wait to work on docs. + +#### Collaboration + +By default, the developer will work on documentation changes independently, but +the developer, PM, or technicial writer can propose a broader collaboration for +any given issue. + +Additionally, technical writers are available for questions at any time. -**Planning** -- Once an issue contains a Documentation label and an upcoming milestone, a -technical writer reviews the listed documentation requirements, which should have -already been reviewed by the PM. (These are non-blocking reviews; developers should -not wait to work on docs.) -- Monitor the documentation needs of issues assigned to the current and next milestone, -and participate in any needed discussion on docs planning with the dev, PM, and others. +#### Review -**Review** - Techncial writers provide non-blocking reviews of all documentation changes, -typically after the change is merged. However, if the docs are ready in the MR while -we are awaiting other work in order to merge, the technical writer's review can commence early. +before or after the change is merged. However, if the docs are ready in the MR while +there's time before the freeze, the technical writer's review can commence early, on request. - The technical writer will confirm that the doc is clear, grammatically correct, and discoverable, while avoiding redundancy, bad file locations, typos, broken links, etc. The technical writer will review the documentation for the following, which the developer and code reviewer should have already made a good-faith effort to ensure: - Clarity. - - Relevance (make sure the content is appropriate given the impact of the feature). - - Location (make sure the doc is in the correct dir and has the correct name). + - Adherence to the plans and goals in the issue. + - Location (make sure the docs are in the correct directorkes and has the correct name). - Syntax, typos, and broken links. - Improvements to the content. - - Accordance to the [Documentation Style Guide](styleguide.md) and [structure/template](structure.md). + - Accordance with the [Documentation Style Guide](styleguide.md), and [Structure and Template](structure.md) doc. diff --git a/doc/development/documentation/improvement-workflow.md b/doc/development/documentation/improvement-workflow.md index ef6392c6f7f..c97d8966b8c 100644 --- a/doc/development/documentation/improvement-workflow.md +++ b/doc/development/documentation/improvement-workflow.md @@ -9,26 +9,30 @@ Anyone can contribute a merge request or create an issue for GitLab's documentat This page covers the process for any contributions to GitLab's docs that are not part of feature development. If you are looking for information on updating GitLab's docs as is required with the development and release of a new feature -or feature enhancement, see the [feature-change documentation workflow](feature-change-workflow.md). +or feature enhancement, see the [documentation process for feature changes](feature-change-workflow.md). ## Who updates the docs Anyone can contribute! You can create a merge request with documentation when you find errors or other room for improvement in an existing doc, or when you -have an idea for all-new documentation that would help a GitLab user or admin -to achieve or improve their DevOps workflows. +have an idea for all-new documentation that would help a GitLab user or administrator +to accomplish their work with GitLab. ## How to update the docs -- Follow the described standards and processes listed on the [GitLab Documentation guidelines](index.md) page, -including linked resources: the [Structure and template](structure.md) page, [Style Guide](styleguide.md), and [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). -- Follow GitLab's [Merge Request Guidelines](../contributing/merge_request_workflow.md#merge-request-guidelines). -- If you need any help to choose the correct place for a doc, discuss a documentation +1. Click "Edit this Page" at the bottom of any page on docs.gitlab.com, or navigate to +one of the repositories and doc paths listed on the [GitLab Documentation guidelines](index.md) page. +Work in a fork if you do not have developer access to the GitLab project. +1. Follow the described standards and processes listed on that Guidelines page, +including the linked resources: the [Structure and template](structure.md) page, [Style Guide](styleguide.md), and [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). +1. Follow GitLab's [Merge Request Guidelines](../contributing/merge_request_workflow.md#merge-request-guidelines). + +If you need any help to choose the correct place for a doc, discuss a documentation idea or outline, or request any other help, ping the Technical Writer for the relevant [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages) in your issue or MR, or write within `#docs` if you are a member of GitLab's Slack workspace. -## Merging +## Review and merging Anyone with master access to the affected GitLab project can merge documentation changes. This person must make a good-faith effort to ensure that the content is clear @@ -38,12 +42,22 @@ that it meets the [Documentation Guidelines](index.md) and [Style Guide](stylegu If the author or reviewer has any questions, or would like a techncial writer's review before merging, mention the writer who is assigned to the relevant [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). -## Technical Writer review +The process can involve the following parties/phases, and is replicated in the `Documentation` MR template for GitLab CE and EE, to help with following the process. + +**1. Primary Reviewer** - Review by a [code reviewer](https://about.gitlab.com/handbook/engineering/projects/) or other appropriate colleague to confirm accuracy, clarity, and completeness. This can be skipped for minor fixes without substantive content changes. + +**2. Technical Writer** - Optional - If not requested for this MR, must be scheduled post-merge. To request a pre-merge review, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). +To request a post-merge review, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review) and link it from the MR that makes the doc change. + +**3. Maintainer** -The technical writing team reviews changes after they are merged, unless a prior -review is requested. +1. Review by assigned maintainer, who can always request/require the above reviews. Maintainer review can occur before or after a technical writer review. +2. Ensure a release milestone of the format XX.Y is set. If the freeze for that release has begun, add the label `pick into <XX.Y>` unless this change is not required for the release. In that case, simply change the milestone. +3. If EE and CE MRs exist, merge the EE MR first, then the CE MR. +4. After merging, if there has not been a technical writer review and an issue for a follow-up review was not already created and linked from the MR, [create the issue using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review) and link it from the MR. ## Other ways to help If you have ideas for further documentation resources that would be best -considered/handled by technical writers, devs, and other SMEs, please create an issue. +considered/handled by technical writers, devs, and other SMEs, please [create an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Documentation) +using the Documentation template. diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 287a472d0d8..652fe3ea711 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -10,8 +10,8 @@ In addition to this page, the following resources to help craft and contribute d - [Style Guide](styleguide.md) - What belongs in the docs, language guidelines, and more. - [Structure and template](structure.md) - Learn the typical parts of a doc page and how to write each one. -- [Workflow](workflow.md) - A landing page for our key workflows: - - [Feature-change documentation workflow](feature-change-workflow.md) - Adding required documentation when developing a GitLab feature. +- [Workflows](workflow.md) - A landing page for our key workflows: + - [Documentation process for feature changes](feature-change-workflow.md) - Adding required documentation when developing a GitLab feature. - [Documentation improvement workflow](improvement-workflow.md) - New content not associated with a new feature. - [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/) - A reference for the markdown implementation used by GitLab's documentation site and about.gitlab.com. - [Site architecture](site_architecture/index.md) - How docs.gitlab.com is built. diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md index 7c32c92b147..0abfe4b82a4 100644 --- a/doc/development/documentation/workflow.md +++ b/doc/development/documentation/workflow.md @@ -2,9 +2,9 @@ description: Learn the processes for contributing to GitLab's documentation. --- -# Documentation workflows at GitLab +# Documentation workflows -Documentation workflows at GitLab differ depending on the reason for the change. The two types of documentation changes are: +Documentation workflows at GitLab differ depending on the reason for the change: -- [Feature-change documentation workflow](feature-change-workflow.md) - The documentation is being created or updated as part of the development and release of a new or enhanced feature. This process involves the developer of the feature (who includes new/updated documentation files as part of the same merge request containing the feature's code) and also involves the product manager and technical writer who are listed for the feature's [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). +- [Documentation process for feature changes](feature-change-workflow.md) - The documentation is being created or updated as part of the development and release of a new or enhanced feature. This process involves the developer of the feature (who includes new/updated documentation files as part of the same merge request containing the feature's code) and also involves the product manager and technical writer who are listed for the feature's [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). - [Documentation improvement workflow](improvement-workflow.md) - All documentation additions not associated with a feature release. Documentation is being created or updated to improve accuracy, completeness, ease of use, or any reason other than a feature change. Anyone (and everyone) can contribute a merge request for this type of change at any time. diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 6012f1080ab..0470a071d39 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -141,14 +141,15 @@ module. GitLab has a custom `spyOnDependency` method which utilizes [babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to achieve this. It can be used like so: -```javascript +```js // my_module.js import { visitUrl } from '~/lib/utils/url_utility'; export default function doSomething() { visitUrl('/foo/bar'); } - +``` +```js // my_module_spec.js import doSomething from '~/my_module'; diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 82ba1d7d984..cd755089be8 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -1,5 +1,7 @@ # Slash Commands +> The `run` command was [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. + Slash commands in Mattermost and Slack allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it. Commands are scoped to a project, with a trigger term that is specified during configuration. @@ -16,6 +18,7 @@ Taking the trigger term as `project-name`, the commands are: | `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` | | `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` | | `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment | +| `/project-name run <job name> <arguments>` | Execute [ChatOps](../ci/chatops/README.md) job `<job name>` on `master` | Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage). diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 81ee7338e4e..8601551e3bd 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -1,6 +1,6 @@ # Public access -GitLab allows you to change your projects' visibility in order be accessed +GitLab allows [Owners](../user/permissions.md) to change a projects' visibility in order to be accessed **publicly** or **internally**. Projects with either of these visibility levels will be listed in the diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 55b678d6af5..a3698d60f6d 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -147,7 +147,7 @@ Please refer to `group_rename` and `user_rename` for that case. "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, - "project_visibility": "private" + "project_visibility": "visibilitylevel|private" } ``` @@ -158,7 +158,7 @@ Please refer to `group_rename` and `user_rename` for that case. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_remove_from_team", - "project_access": "Maintainer", + "access_level": "Maintainer", "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", @@ -167,7 +167,7 @@ Please refer to `group_rename` and `user_rename` for that case. "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, - "project_visibility": "private" + "project_visibility": "visibilitylevel|private" } ``` diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 7387d1810ca..db68510c46d 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -93,6 +93,12 @@ You can choose between 3 options: ## Localization +### Language + +Select your preferred language from a list of supported languages. + +*This feature is experimental and translations are not complete yet.* + ### First day of the week The first day of the week can be customised for calendar views and date pickers. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 85a4af24dc5..42aa6840609 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -307,6 +307,28 @@ you should be careful as GitLab cannot detect it. In this case, installing Tiller via the applications will result in the cluster having it twice, which can lead to confusion during deployments. +### Upgrading applications + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24789) +in GitLab 11.8. + +Users can perform a one-click upgrade for the GitLab Runner application, +when there is an upgrade available. + +To upgrade the GitLab Runner application: + +1. Navigate to your project's **Operations > Kubernetes**. +1. Select your cluster. +1. Click the **Upgrade** button for the Runnner application. + +The **Upgrade** button will not be shown if there is no upgrade +available. + +NOTE: **Note:** +Upgrades will reset values back to the values built into the `runner` +chart plus the values set by +[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml) + ## Getting the external IP address NOTE: **Note:** diff --git a/jest.config.js b/jest.config.js index 4dab7c2891a..fac2e435cef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,4 +31,5 @@ module.exports = { '^.+\\.js$': 'babel-jest', '^.+\\.vue$': 'vue-jest', }, + transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui)/)'], }; diff --git a/lib/gitlab/chat.rb b/lib/gitlab/chat.rb new file mode 100644 index 00000000000..23d4fb36b66 --- /dev/null +++ b/lib/gitlab/chat.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Returns `true` if Chatops is available for the current instance. + def self.available? + ::Feature.enabled?(:chatops, default_enabled: true) + end + end +end diff --git a/lib/gitlab/chat/command.rb b/lib/gitlab/chat/command.rb new file mode 100644 index 00000000000..49b7dcf4bbe --- /dev/null +++ b/lib/gitlab/chat/command.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Class for scheduling chat pipelines. + # + # A Command takes care of creating a `Ci::Pipeline` with all the data + # necessary to execute a chat command. This includes data such as the chat + # data (e.g. the response URL) and any environment variables that should be + # exposed to the chat command. + class Command + include Utils::StrongMemoize + + attr_reader :project, :chat_name, :name, :arguments, :response_url, + :channel + + # project - The Project to schedule the command for. + # chat_name - The ChatName belonging to the user that scheduled the + # command. + # name - The name of the chat command to run. + # arguments - The arguments (as a String) to pass to the command. + # channel - The channel the message was sent from. + # response_url - The URL to send the response back to. + def initialize(project:, chat_name:, name:, arguments:, channel:, response_url:) + @project = project + @chat_name = chat_name + @name = name + @arguments = arguments + @channel = channel + @response_url = response_url + end + + # Tries to create a new pipeline. + # + # This method will return a pipeline that _may_ be persisted, or `nil` if + # the pipeline could not be created. + def try_create_pipeline + return unless valid? + + create_pipeline + end + + def create_pipeline + service = ::Ci::CreatePipelineService.new( + project, + chat_name.user, + ref: branch, + sha: commit, + chat_data: { + chat_name_id: chat_name.id, + command: name, + arguments: arguments, + response_url: response_url + } + ) + + service.execute(:chat) do |pipeline| + build_environment_variables(pipeline) + build_chat_data(pipeline) + end + end + + # pipeline - The `Ci::Pipeline` to create the environment variables for. + def build_environment_variables(pipeline) + pipeline.variables.build( + [{ key: 'CHAT_INPUT', value: arguments }, + { key: 'CHAT_CHANNEL', value: channel }] + ) + end + + # pipeline - The `Ci::Pipeline` to create the chat data for. + def build_chat_data(pipeline) + pipeline.build_chat_data( + chat_name_id: chat_name.id, + response_url: response_url + ) + end + + def valid? + branch && commit + end + + def branch + strong_memoize(:branch) { project.default_branch } + end + + def commit + strong_memoize(:commit) do + project.commit(branch)&.id if branch + end + end + end + end +end diff --git a/lib/gitlab/chat/output.rb b/lib/gitlab/chat/output.rb new file mode 100644 index 00000000000..411b1555a7d --- /dev/null +++ b/lib/gitlab/chat/output.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + # Class for gathering and formatting the output of a `Ci::Build`. + class Output + attr_reader :build + + MissingBuildSectionError = Class.new(StandardError) + + # The primary trace section to look for. + PRIMARY_SECTION = 'chat_reply' + + # The backup trace section in case the primary one could not be found. + FALLBACK_SECTION = 'build_script' + + # build - The `Ci::Build` to obtain the output from. + def initialize(build) + @build = build + end + + # Returns a `String` containing the output of the build. + # + # The output _does not_ include the command that was executed. + def to_s + offset, length = read_offset_and_length + + trace.read do |stream| + stream.seek(offset) + + output = stream + .stream + .read(length) + .force_encoding(Encoding.default_external) + + without_executed_command_line(output) + end + end + + # Returns the offset to seek to and the number of bytes to read relative + # to the offset. + def read_offset_and_length + section = find_build_trace_section(PRIMARY_SECTION) || + find_build_trace_section(FALLBACK_SECTION) + + unless section + raise( + MissingBuildSectionError, + "The build_script trace section could not be found for build #{build.id}" + ) + end + + length = section[:byte_end] - section[:byte_start] + + [section[:byte_start], length] + end + + # Removes the line containing the executed command from the build output. + # + # output - A `String` containing the output of a trace section. + def without_executed_command_line(output) + # If `output.split("\n")` produces an empty Array then the slicing that + # follows it will produce a nil. For example: + # + # "\n".split("\n") # => [] + # "\n".split("\n")[1..-1] # => nil + # + # To work around this we only "join" if we're given an Array. + if (converted = output.split("\n")[1..-1]) + converted.join("\n") + else + '' + end + end + + # Returns the trace section for the given name, or `nil` if the section + # could not be found. + # + # name - The name of the trace section to find. + def find_build_trace_section(name) + trace_sections.find { |s| s[:name] == name } + end + + def trace_sections + @trace_sections ||= trace.extract_sections + end + + def trace + @trace ||= build.trace + end + end + end +end diff --git a/lib/gitlab/chat/responder.rb b/lib/gitlab/chat/responder.rb new file mode 100644 index 00000000000..6267fbc20e2 --- /dev/null +++ b/lib/gitlab/chat/responder.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + # Returns an instance of the responder to use for generating chat + # responses. + # + # This method will return `nil` if no formatter is available for the given + # build. + # + # build - A `Ci::Build` that executed a chat command. + def self.responder_for(build) + service = build.pipeline.chat_data&.chat_name&.service + + if (responder = service.try(:chat_responder)) + responder.new(build) + end + end + end + end +end diff --git a/lib/gitlab/chat/responder/base.rb b/lib/gitlab/chat/responder/base.rb new file mode 100644 index 00000000000..f1ad0e36793 --- /dev/null +++ b/lib/gitlab/chat/responder/base.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + class Base + attr_reader :build + + # build - The `Ci::Build` that was executed. + def initialize(build) + @build = build + end + + def pipeline + build.pipeline + end + + def project + pipeline.project + end + + def success(*) + raise NotImplementedError, 'You must implement #success(output)' + end + + def failure + raise NotImplementedError, 'You must implement #failure' + end + + def send_response(output) + raise NotImplementedError, 'You must implement #send_response(output)' + end + + def scheduled_output + raise NotImplementedError, 'You must implement #scheduled_output' + end + end + end + end +end diff --git a/lib/gitlab/chat/responder/slack.rb b/lib/gitlab/chat/responder/slack.rb new file mode 100644 index 00000000000..0cf02c92a67 --- /dev/null +++ b/lib/gitlab/chat/responder/slack.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Gitlab + module Chat + module Responder + class Slack < Responder::Base + SUCCESS_COLOR = '#B3ED8E' + FAILURE_COLOR = '#FF5640' + RESPONSE_TYPE = :in_channel + + # Slack breaks messages apart if they're around 4 KB in size. We use a + # slightly smaller limit here to account for user mentions. + MESSAGE_SIZE_LIMIT = 3.5.kilobytes + + # Sends a response back to Slack + # + # output - The output to send back to Slack, as a Hash. + def send_response(output) + Gitlab::HTTP.post( + pipeline.chat_data.response_url, + { + headers: { Accept: 'application/json' }, + body: output.to_json + } + ) + end + + # Sends the output for a build that completed successfully. + # + # output - The output produced by the chat command. + def success(output) + return if output.empty? + + send_response( + text: message_text(limit_output(output)), + response_type: RESPONSE_TYPE + ) + end + + # Sends the output for a build that failed. + def failure + send_response( + text: message_text("<#{build_url}|Sorry, the build failed!>"), + response_type: RESPONSE_TYPE + ) + end + + # Returns the output to send back after a command has been scheduled. + def scheduled_output + # We return an empty message so that Slack still shows the input + # command, without polluting the channel with standard "The job has + # been scheduled" (or similar) responses. + { text: '' } + end + + private + + def limit_output(output) + if output.bytesize <= MESSAGE_SIZE_LIMIT + output + else + "<#{build_url}|The output is too large to be sent back directly!>" + end + end + + def mention_user + "<@#{pipeline.chat_data.chat_name.chat_id}>" + end + + def message_text(output) + "#{mention_user}: #{output}" + end + + def build_url + ::Gitlab::Routing.url_helpers.project_build_url(project, build) + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index e62d547d862..e4ed1424865 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -10,7 +10,8 @@ module Gitlab :origin_ref, :checkout_sha, :after_sha, :before_sha, :trigger_request, :schedule, :merge_request, :ignore_skip_ci, :save_incompleted, - :seeds_block, :variables_attributes, :push_options + :seeds_block, :variables_attributes, :push_options, + :chat_data ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb index 0f687a4ce9b..1e09b417311 100644 --- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb +++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb @@ -6,7 +6,13 @@ module Gitlab module Chain class RemoveUnwantedChatJobs < Chain::Base def perform! - # to be overriden in EE + return unless pipeline.config_processor && pipeline.chat? + + # When scheduling a chat pipeline we only want to run the build + # that matches the chat command. + pipeline.config_processor.jobs.select! do |name, _| + name.to_s == command.chat_data[:command].to_s + end end def break? diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index 0f3f5cb3c08..d3c86fdb629 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -106,13 +106,13 @@ module Gitlab %r{\A(ee/)?app/(assets|views)/} => :frontend, %r{\A(ee/)?public/} => :frontend, - %r{\A(ee/)?spec/javascripts/} => :frontend, + %r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend, %r{\A(ee/)?vendor/assets/} => :frontend, %r{\A(jest\.config\.js|package\.json|yarn\.lock)\z} => :frontend, %r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend, %r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend, - %r{\A(ee/)?spec/(?!javascripts)[^/]+} => :backend, + %r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend, %r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend, %r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend, %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile|\.gitlab-ci\.yml)\z} => :backend, diff --git a/lib/gitlab/slash_commands/application_help.rb b/lib/gitlab/slash_commands/application_help.rb new file mode 100644 index 00000000000..0ea7554ba64 --- /dev/null +++ b/lib/gitlab/slash_commands/application_help.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + class ApplicationHelp < BaseCommand + def initialize(params) + @params = params + end + + def execute + Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, params[:text]) + end + + private + + def trigger + "#{params[:command]} [project name or alias]" + end + + def commands + Gitlab::SlashCommands::Command.commands + end + end + end +end diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 474c09b9c4d..7c963fcf38a 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -9,7 +9,8 @@ module Gitlab Gitlab::SlashCommands::IssueNew, Gitlab::SlashCommands::IssueSearch, Gitlab::SlashCommands::IssueMove, - Gitlab::SlashCommands::Deploy + Gitlab::SlashCommands::Deploy, + Gitlab::SlashCommands::Run ] end diff --git a/lib/gitlab/slash_commands/presenters/error.rb b/lib/gitlab/slash_commands/presenters/error.rb new file mode 100644 index 00000000000..442f8796338 --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/error.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + module Presenters + class Error < Presenters::Base + def initialize(message) + @message = message + end + + def message + ephemeral_response(text: @message) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/presenters/run.rb b/lib/gitlab/slash_commands/presenters/run.rb new file mode 100644 index 00000000000..c4bbc231464 --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/run.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + module Presenters + class Run < Presenters::Base + # rubocop: disable CodeReuse/ActiveRecord + def present(pipeline) + build = pipeline.builds.take + + if build && (responder = Chat::Responder.responder_for(build)) + in_channel_response(responder.scheduled_output) + else + unsupported_chat_service + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def unsupported_chat_service + ephemeral_response(text: 'Sorry, this chat service is currently not supported by GitLab ChatOps.') + end + + def failed_to_schedule(command) + ephemeral_response( + text: 'The command could not be scheduled. Make sure that your ' \ + 'project has a .gitlab-ci.yml that defines a job with the ' \ + "name #{command.inspect}" + ) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/run.rb b/lib/gitlab/slash_commands/run.rb new file mode 100644 index 00000000000..10a545e28ac --- /dev/null +++ b/lib/gitlab/slash_commands/run.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module SlashCommands + # Slash command for triggering chatops jobs. + class Run < BaseCommand + def self.match(text) + /\Arun\s+(?<command>\S+)(\s+(?<arguments>.+))?\z/.match(text) + end + + def self.help_message + 'run <command> <arguments>' + end + + def self.available?(project) + Chat.available? && project.builds_enabled? + end + + def self.allowed?(project, user) + can?(user, :create_pipeline, project) + end + + def execute(match) + command = Chat::Command.new( + project: project, + chat_name: chat_name, + name: match[:command], + arguments: match[:arguments], + channel: params[:channel_id], + response_url: params[:response_url] + ) + + presenter = Gitlab::SlashCommands::Presenters::Run.new + pipeline = command.try_create_pipeline + + if pipeline&.persisted? + presenter.present(pipeline) + else + presenter.failed_to_schedule(command.name) + end + end + end + end +end diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 4187014d49e..49ec196b103 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -54,7 +54,7 @@ module Sentry def handle_response(response) unless response.code == 200 - raise Client::Error, "Sentry response error: #{response.code}" + raise Client::Error, "Sentry response status code: #{response.code}" end response.as_json diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 973950d27aa..fa680cd5ef0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1542,9 +1542,6 @@ msgstr "" msgid "Closed" msgstr "" -msgid "Closed (moved)" -msgstr "" - msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgstr "" @@ -3295,7 +3292,7 @@ msgstr "" msgid "Failed to load emoji list." msgstr "" -msgid "Failed to load errors from Sentry" +msgid "Failed to load errors from Sentry. Error message: %{errorMessage}" msgstr "" msgid "Failed to remove issue from board, please try again." @@ -4097,6 +4094,9 @@ msgstr "" msgid "Invoke Time" msgstr "" +msgid "IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})" +msgstr "" + msgid "Issue" msgstr "" @@ -4115,7 +4115,7 @@ msgstr "" msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable." msgstr "" -msgid "Issues, merge requests, pushes and comments." +msgid "Issues, merge requests, pushes, and comments." msgstr "" msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected." @@ -4271,6 +4271,9 @@ msgstr "" msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed." msgstr "" +msgid "Language" +msgstr "" + msgid "Large File Storage" msgstr "" @@ -5530,6 +5533,9 @@ msgstr "" msgid "Preferences|Navigation theme" msgstr "" +msgid "Preferences|This feature is experimental and translations are not complete yet" +msgstr "" + msgid "Press Enter or click to search" msgstr "" @@ -5722,9 +5728,6 @@ msgstr "" msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgstr "" -msgid "Profiles|This feature is experimental and translations are not complete yet" -msgstr "" - msgid "Profiles|This information will appear on your profile" msgstr "" diff --git a/package.json b/package.json index 9df2a3ccff7..1a6538cf791 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", - "@gitlab/svgs": "^1.52.0", + "@gitlab/svgs": "^1.53.0", "@gitlab/ui": "^2.0.4", "apollo-boost": "^0.1.20", "apollo-client": "^2.4.5", @@ -260,6 +260,10 @@ module QA autoload :Sidebar, 'qa/page/issuable/sidebar' end + module Alert + autoload :AutoDevopsAlert, 'qa/page/alert/auto_devops_alert' + end + module Layout autoload :Banner, 'qa/page/layout/banner' end diff --git a/qa/qa/page/alert/auto_devops_alert.rb b/qa/qa/page/alert/auto_devops_alert.rb new file mode 100644 index 00000000000..8f66c805b77 --- /dev/null +++ b/qa/qa/page/alert/auto_devops_alert.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Page + module Alert + class AutoDevopsAlert < Page::Base + view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do + element :auto_devops_banner + end + end + end + end +end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index f192f1fc64b..22b0a168964 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -18,6 +18,10 @@ module QA::Page element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern end + view 'app/views/projects/pipelines/_info.html.haml' do + element :pipeline_badges + end + def running? within('.ci-header-container') do page.has_content?('running') @@ -32,6 +36,12 @@ module QA::Page end end + def has_tag?(tag_name) + within_element(:pipeline_badges) do + has_selector?('.badge', text: tag_name) + end + end + def go_to_job(job_name) find_element(:job_link, job_name).click end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 8b165a04382..02bd378acfc 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -4,13 +4,13 @@ require 'pathname' module QA # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68 - context 'Configure', :orchestrated, :kubernetes, :quarantine do - describe 'Auto DevOps support' do - def login - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) - end + context 'Configure' do + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + end + describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do [true, false].each do |rbac| context "when rbac is #{rbac ? 'enabled' : 'disabled'}" do before(:all) do @@ -173,5 +173,38 @@ module QA end end end + + describe 'Auto DevOps', :smoke do + it 'enables AutoDevOps by default' do + login + + project = Resource::Project.fabricate! do |p| + p.name = Runtime::Env.auto_devops_project_name || 'project-with-autodevops' + p.description = 'Project with AutoDevOps' + end + + project.visit! + + Page::Alert::AutoDevopsAlert.perform do |alert| + expect(alert).to have_text(/.*The Auto DevOps pipeline has been enabled.*/) + end + + # Create AutoDevOps repo + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.directory = Pathname + .new(__dir__) + .join('../../../../../fixtures/auto_devops_rack') + push.commit_message = 'Create AutoDevOps compatible Project' + end + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline) + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to have_tag('Auto DevOps') + end + end + end end end diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 848364b4a9b..bc73225c1bf 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -35,7 +35,7 @@ fi # Do not use 'README.md', instead use 'index.md' # Number of 'README.md's as of 2018-03-26 -NUMBER_READMES_CE=42 +NUMBER_READMES_CE=43 NUMBER_READMES_EE=46 FIND_READMES=$(find doc/ -name "README.md" | wc -l) echo '=> Checking for new README.md files...' diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 760c0fab130..ee881f85233 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -43,7 +43,8 @@ describe Profiles::PreferencesController do color_scheme_id: '1', dashboard: 'stars', theme_id: '2', - first_day_of_week: '1' + first_day_of_week: '1', + preferred_language: 'jp' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!) diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 90d6841af0e..9909bfb5904 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'User visits the profile preferences page' do + include Select2Helper + let(:user) { create(:user) } before do @@ -60,6 +62,28 @@ describe 'User visits the profile preferences page' do end end + describe 'User changes their language', :js do + it 'creates a flash message' do + select2('en', from: '#user_preferred_language') + click_button 'Save' + + wait_for_requests + + expect_preferences_saved_message + end + + it 'updates their preference' do + wait_for_requests + select2('eo', from: '#user_preferred_language') + click_button 'Save' + + wait_for_requests + refresh + + expect(page).to have_css('html[lang="eo"]') + end + end + def expect_preferences_saved_message page.within('.flash-container') do expect(page).to have_content('Preferences saved.') diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb index a4ad331f613..e074e53c2c5 100644 --- a/spec/finders/concerns/finder_methods_spec.rb +++ b/spec/finders/concerns/finder_methods_spec.rb @@ -12,7 +12,7 @@ describe FinderMethods do end def execute - Project.all + Project.all.order(id: :desc) end end end @@ -38,6 +38,16 @@ describe FinderMethods do it 'raises not found the user does not have access' do expect { finder.find_by!(id: unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'ignores ordering' do + # Memoise the finder result so we can add message expectations to it + relation = finder.execute + allow(finder).to receive(:execute).and_return(relation) + + expect(relation).to receive(:reorder).with(nil).and_call_original + + finder.find_by!(id: authorized_project.id) + end end describe '#find' do @@ -66,5 +76,15 @@ describe FinderMethods do it 'returns nil when the user does not have access' do expect(finder.find_by(id: unauthorized_project.id)).to be_nil end + + it 'ignores ordering' do + # Memoise the finder result so we can add message expectations to it + relation = finder.execute + allow(finder).to receive(:execute).and_return(relation) + + expect(relation).to receive(:reorder).with(nil).and_call_original + + finder.find_by(id: authorized_project.id) + end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 34cb09942be..fe8000e419b 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -416,6 +416,36 @@ describe IssuesFinder do end end + context 'filtering by closed_at' do + let!(:closed_issue1) { create(:issue, project: project1, state: :closed, closed_at: 1.week.ago) } + let!(:closed_issue2) { create(:issue, project: project2, state: :closed, closed_at: 1.week.from_now) } + let!(:closed_issue3) { create(:issue, project: project2, state: :closed, closed_at: 2.weeks.from_now) } + + context 'through closed_after' do + let(:params) { { state: :closed, closed_after: closed_issue3.closed_at } } + + it 'returns issues closed on or after the given date' do + expect(issues).to contain_exactly(closed_issue3) + end + end + + context 'through closed_before' do + let(:params) { { state: :closed, closed_before: closed_issue1.closed_at } } + + it 'returns issues closed on or before the given date' do + expect(issues).to contain_exactly(closed_issue1) + end + end + + context 'through closed_after and closed_before' do + let(:params) { { state: :closed, closed_after: closed_issue2.closed_at, closed_before: closed_issue3.closed_at } } + + it 'returns issues closed between the given dates' do + expect(issues).to contain_exactly(closed_issue2, closed_issue3) + end + end + end + context 'filtering by reaction name' do context 'user searches by no reaction' do let(:params) { { my_reaction_emoji: 'None' } } diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js index dc0b04173bf..e2ee70b9d69 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/frontend/lib/utils/ajax_cache_spec.js @@ -94,68 +94,54 @@ describe('AjaxCache', () => { beforeEach(() => { mock = new MockAdapter(axios); - spyOn(axios, 'get').and.callThrough(); + jest.spyOn(axios, 'get'); }); afterEach(() => { mock.restore(); }); - it('stores and returns data from Ajax call if cache is empty', done => { + it('stores and returns data from Ajax call if cache is empty', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toEqual(dummyResponse); - expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); - }) - .then(done) - .catch(fail); + return AjaxCache.retrieve(dummyEndpoint).then(data => { + expect(data).toEqual(dummyResponse); + expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); + }); }); - it('makes no Ajax call if request is pending', done => { + it('makes no Ajax call if request is pending', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - AjaxCache.retrieve(dummyEndpoint) - .then(done) - .catch(fail); - - AjaxCache.retrieve(dummyEndpoint) - .then(done) - .catch(fail); - - expect(axios.get.calls.count()).toBe(1); + return Promise.all([ + AjaxCache.retrieve(dummyEndpoint), + AjaxCache.retrieve(dummyEndpoint), + ]).then(() => { + expect(axios.get).toHaveBeenCalledTimes(1); + }); }); - it('returns undefined if Ajax call fails and cache is empty', done => { + it('returns undefined if Ajax call fails and cache is empty', () => { const errorMessage = 'Network Error'; mock.onGet(dummyEndpoint).networkError(); - AjaxCache.retrieve(dummyEndpoint) - .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`)) - .catch(error => { - expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); - expect(error.textStatus).toBe(errorMessage); - done(); - }) - .catch(fail); + expect.assertions(2); + return AjaxCache.retrieve(dummyEndpoint).catch(error => { + expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); + expect(error.textStatus).toBe(errorMessage); + }); }); - it('makes no Ajax call if matching data exists', done => { + it('makes no Ajax call if matching data exists', () => { AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; - mock.onGet(dummyEndpoint).reply(() => { - fail(new Error('expected no Ajax call!')); - }); - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toBe(dummyResponse); - }) - .then(done) - .catch(fail); + return AjaxCache.retrieve(dummyEndpoint).then(data => { + expect(data).toBe(dummyResponse); + expect(axios.get).not.toHaveBeenCalled(); + }); }); - it('makes Ajax call even if matching data exists when forceRequest parameter is provided', done => { + it('makes Ajax call even if matching data exists when forceRequest parameter is provided', () => { const oldDummyResponse = { important: 'old dummy data', }; @@ -164,21 +150,12 @@ describe('AjaxCache', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - // Call without forceRetrieve param - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toBe(oldDummyResponse); - }) - .then(done) - .catch(fail); - - // Call with forceRetrieve param - AjaxCache.retrieve(dummyEndpoint, true) - .then(data => { - expect(data).toEqual(dummyResponse); - }) - .then(done) - .catch(fail); + return Promise.all([ + AjaxCache.retrieve(dummyEndpoint), + AjaxCache.retrieve(dummyEndpoint, true), + ]).then(data => { + expect(data).toEqual([oldDummyResponse, dummyResponse]); + }); }); }); }); diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap new file mode 100644 index 00000000000..11d65ced180 --- /dev/null +++ b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JumpToNextDiscussionButton matches the snapshot 1`] = ` +<div + class="btn-group" + role="group" +> + <button + class="btn btn-default discussion-next-btn" + data-original-title="Jump to next unresolved discussion" + title="" + > + <icon-stub + cssclasses="" + name="comment-next" + size="16" + /> + </button> +</div> +`; diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js new file mode 100644 index 00000000000..989b0458481 --- /dev/null +++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js @@ -0,0 +1,30 @@ +import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue'; +import { shallowMount } from '@vue/test-utils'; + +describe('JumpToNextDiscussionButton', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(JumpToNextDiscussionButton, { + sync: false, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('matches the snapshot', () => { + expect(wrapper.vm.$el).toMatchSnapshot(); + }); + + it('emits onClick event on button click', () => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + expect(wrapper.emitted()).toEqual({ + onClick: [[]], + }); + }); +}); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 7ad2e97e7e6..d892889b98d 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -1,3 +1,7 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import axios from '~/lib/utils/axios_utils'; + const testTimeoutInMs = 300; jest.setTimeout(testTimeoutInMs); @@ -14,3 +18,17 @@ afterEach(() => { throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`); } }); + +// fail tests for unmocked requests +beforeEach(done => { + axios.defaults.adapter = config => { + const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`); + error.config = config; + done.fail(error); + return Promise.reject(error); + }; + + done(); +}); + +Vue.use(Translate); diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 66de372e9fe..5f9c180cbb7 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -5,16 +5,63 @@ describe Resolvers::IssuesResolver do let(:current_user) { create(:user) } set(:project) { create(:project) } - set(:issue) { create(:issue, project: project) } - set(:issue2) { create(:issue, project: project, title: 'foo') } + set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) } + set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) } + set(:label1) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } before do project.add_developer(current_user) + create(:label_link, label: label1, target: issue1) + create(:label_link, label: label1, target: issue2) + create(:label_link, label: label2, target: issue2) end describe '#resolve' do it 'finds all issues' do - expect(resolve_issues).to contain_exactly(issue, issue2) + expect(resolve_issues).to contain_exactly(issue1, issue2) + end + + it 'filters by state' do + expect(resolve_issues(state: 'opened')).to contain_exactly(issue1) + expect(resolve_issues(state: 'closed')).to contain_exactly(issue2) + end + + it 'filters by labels' do + expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2) + expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) + end + + describe 'filters by created_at' do + it 'filters by created_before' do + expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1) + end + + it 'filters by created_after' do + expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2) + end + end + + describe 'filters by updated_at' do + it 'filters by updated_before' do + expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1) + end + + it 'filters by updated_after' do + expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2) + end + end + + describe 'filters by closed_at' do + let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) } + + it 'filters by closed_before' do + expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3) + end + + it 'filters by closed_after' do + expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2) + end end it 'searches issues' do @@ -22,7 +69,7 @@ describe Resolvers::IssuesResolver do end it 'sort issues' do - expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue] + expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1] end it 'returns issues user can see' do @@ -30,31 +77,31 @@ describe Resolvers::IssuesResolver do create(:issue, confidential: true) - expect(resolve_issues).to contain_exactly(issue, issue2) + expect(resolve_issues).to contain_exactly(issue1, issue2) end it 'finds a specific issue with iid' do - expect(resolve_issues(iid: issue.iid)).to contain_exactly(issue) + expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1) end it 'finds a specific issue with iids' do - expect(resolve_issues(iids: issue.iid)).to contain_exactly(issue) + expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1) end it 'finds multiple issues with iids' do - expect(resolve_issues(iids: [issue.iid, issue2.iid])) - .to contain_exactly(issue, issue2) + expect(resolve_issues(iids: [issue1.iid, issue2.iid])) + .to contain_exactly(issue1, issue2) end it 'finds only the issues within the project we are looking at' do another_project = create(:project) - iids = [issue, issue2].map(&:iid) + iids = [issue1, issue2].map(&:iid) iids.each do |iid| create(:issue, project: another_project, iid: iid) end - expect(resolve_issues(iids: iids)).to contain_exactly(issue, issue2) + expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2) end end diff --git a/spec/graphql/types/issuable_state_enum_spec.rb b/spec/graphql/types/issuable_state_enum_spec.rb new file mode 100644 index 00000000000..65a80fa4176 --- /dev/null +++ b/spec/graphql/types/issuable_state_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['IssuableState'] do + it { expect(described_class.graphql_name).to eq('IssuableState') } + + it_behaves_like 'issuable state' +end diff --git a/spec/graphql/types/issue_state_enum_spec.rb b/spec/graphql/types/issue_state_enum_spec.rb new file mode 100644 index 00000000000..de19e6fc505 --- /dev/null +++ b/spec/graphql/types/issue_state_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['IssueState'] do + it { expect(described_class.graphql_name).to eq('IssueState') } + + it_behaves_like 'issuable state' +end diff --git a/spec/graphql/types/merge_request_state_enum_spec.rb b/spec/graphql/types/merge_request_state_enum_spec.rb new file mode 100644 index 00000000000..626e33b18d3 --- /dev/null +++ b/spec/graphql/types/merge_request_state_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['MergeRequestState'] do + it { expect(described_class.graphql_name).to eq('MergeRequestState') } + + it_behaves_like 'issuable state' + + it 'exposes all the existing merge request states' do + expect(described_class.values.keys).to include('merged') + end +end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 4c395248644..e0e8ebd0c3c 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -110,6 +110,13 @@ describe PreferencesHelper do end end + describe '#language_choices' do + it 'returns an array of all available languages' do + expect(helper.language_choices).to be_an(Array) + expect(helper.language_choices.map(&:second)).to eq(Gitlab::I18n.available_locales) + end + end + def stub_user(messages = {}) if messages.empty? allow(helper).to receive(:current_user).and_return(nil) diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb index 1b2a3b484bb..cd66d98f92a 100644 --- a/spec/javascripts/fixtures/blob.rb +++ b/spec/javascripts/fixtures/blob.rb @@ -15,6 +15,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do before do sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end after do diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb index f0e4bb50c67..295f13b34a4 100644 --- a/spec/javascripts/fixtures/commit.rb +++ b/spec/javascripts/fixtures/commit.rb @@ -16,6 +16,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller before do project.add_maintainer(user) sign_in(user) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end it 'commit/show.html.raw' do |example| diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb index efbda955972..a333d9c0150 100644 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ b/spec/javascripts/fixtures/deploy_keys.rb @@ -25,7 +25,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control render_views it 'deploy_keys/keys.json' do |example| - create(:deploy_key, public: true) + create(:rsa_deploy_key_2048, public: true) project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') create(:deploy_keys_project, project: project, deploy_key: project_key) diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb index f8d55fc97c3..03136f4e661 100644 --- a/spec/javascripts/fixtures/groups.rb +++ b/spec/javascripts/fixtures/groups.rb @@ -4,7 +4,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } - let(:group) { create(:group, name: 'frontend-fixtures-group' )} + let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')} render_views diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 18fb1bebf8b..9b8e90c2a43 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:admin) { create(:admin) } + let(:admin) { create(:admin, feed_token: 'feedtoken:coldfeed') } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 26e81f06c0b..eb37be87e1d 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -35,6 +35,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont before do sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) end after do @@ -54,8 +55,10 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont end it 'merge_requests/merged_merge_request.html.raw' do |example| - allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true) - allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true) + expect_next_instance_of(MergeRequest) do |merge_request| + allow(merge_request).to receive(:source_branch_exists?).and_return(true) + allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) + end render_merge_request(example.description, merged_merge_request) end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 9b48646f8f0..85f02923804 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers + runners_token = 'runnerstoken:intabulasreferre' + let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) } let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } - let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } - let!(:variable1) { create(:ci_variable, project: project_variable_populated) } - let!(:variable2) { create(:ci_variable, project: project_variable_populated) } + let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } render_views @@ -20,6 +20,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do before do project.add_maintainer(admin) sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end after do @@ -70,6 +71,9 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end it 'projects/ci_cd_settings_with_variables.html.raw' do |example| + create(:ci_variable, project: project_variable_populated) + create(:ci_variable, project: project_variable_populated) + get :show, params: { namespace_id: project_variable_populated.namespace.to_param, project_id: project_variable_populated diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index a14837e4d4a..bcd6546f3df 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -7,7 +7,6 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } - let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') } render_views @@ -17,6 +16,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do before do sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) end after do @@ -24,6 +24,8 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do end it 'snippets/show.html.raw' do |example| + create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') + get(:show, params: { id: snippet.to_param }) expect(response).to be_success diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb index f0aa874bf75..5cdbadef639 100644 --- a/spec/javascripts/fixtures/u2f.rb +++ b/spec/javascripts/fixtures/u2f.rb @@ -3,7 +3,7 @@ require 'spec_helper' context 'U2F' do include JavaScriptFixturesHelpers - let(:user) { create(:user, :two_factor_via_u2f) } + let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') } before(:all) do clean_frontend_fixtures('u2f/') @@ -33,6 +33,7 @@ context 'U2F' do before do sign_in(user) + allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end it 'u2f/register.html.raw' do |example| diff --git a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js deleted file mode 100644 index c41b29fa788..00000000000 --- a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import jumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; - -const localVue = createLocalVue(); - -describe('jumpToNextDiscussionButton', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallowMount(jumpToNextDiscussionButton, { - localVue, - sync: false, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('emits onClick event on button click', done => { - const button = wrapper.find({ ref: 'button' }); - - button.trigger('click'); - - localVue.nextTick(() => { - expect(wrapper.emitted()).toEqual({ - onClick: [[]], - }); - - done(); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d5c0bf6b25d..82f58dafc78 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -83,6 +83,8 @@ describe('note_app', () => { describe('render', () => { beforeEach(() => { + setFixtures('<div class="js-discussions-count"></div>'); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); wrapper = mountComponent(); }); @@ -127,6 +129,14 @@ describe('note_app', () => { it('should render form comment button as disabled', () => { expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); }); + + it('updates discussions badge', done => { + setTimeout(() => { + expect(document.querySelector('.js-discussions-count').textContent).toEqual('2'); + + done(); + }); + }); }); describe('while fetching data', () => { diff --git a/spec/lib/gitlab/chat/command_spec.rb b/spec/lib/gitlab/chat/command_spec.rb new file mode 100644 index 00000000000..46d23ab2b62 --- /dev/null +++ b/spec/lib/gitlab/chat/command_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Command do + let(:chat_name) { create(:chat_name) } + + let(:command) do + described_class.new( + project: project, + chat_name: chat_name, + name: 'spinach', + arguments: 'foo', + channel: '123', + response_url: 'http://example.com' + ) + end + + describe '#try_create_pipeline' do + let(:project) { create(:project) } + + it 'returns nil when the command is not valid' do + expect(command) + .to receive(:valid?) + .and_return(false) + + expect(command.try_create_pipeline).to be_nil + end + + it 'tries to create the pipeline when a command is valid' do + expect(command) + .to receive(:valid?) + .and_return(true) + + expect(command) + .to receive(:create_pipeline) + + command.try_create_pipeline + end + end + + describe '#create_pipeline' do + let(:project) { create(:project, :test_repo) } + let(:pipeline) { command.create_pipeline } + + before do + stub_repository_ci_yaml_file(sha: project.commit.id) + + project.add_developer(chat_name.user) + end + + it 'creates the pipeline' do + expect(pipeline).to be_persisted + end + + it 'creates the chat data for the pipeline' do + expect(pipeline.chat_data).to be_an_instance_of(Ci::PipelineChatData) + end + + it 'stores the chat name ID in the chat data' do + expect(pipeline.chat_data.chat_name_id).to eq(chat_name.id) + end + + it 'stores the response URL in the chat data' do + expect(pipeline.chat_data.response_url).to eq('http://example.com') + end + + it 'creates the environment variables for the pipeline' do + vars = pipeline.variables.each_with_object({}) do |row, hash| + hash[row.key] = row.value + end + + expect(vars['CHAT_INPUT']).to eq('foo') + expect(vars['CHAT_CHANNEL']).to eq('123') + end + end +end diff --git a/spec/lib/gitlab/chat/output_spec.rb b/spec/lib/gitlab/chat/output_spec.rb new file mode 100644 index 00000000000..b179f9e9d0a --- /dev/null +++ b/spec/lib/gitlab/chat/output_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Output do + let(:build) do + create(:ci_build, pipeline: create(:ci_pipeline, source: :chat)) + end + + let(:output) { described_class.new(build) } + + describe '#to_s' do + it 'returns the build output as a String' do + trace = Gitlab::Ci::Trace.new(build) + + trace.set("echo hello\nhello") + + allow(build) + .to receive(:trace) + .and_return(trace) + + allow(output) + .to receive(:read_offset_and_length) + .and_return([0, 13]) + + expect(output.to_s).to eq('he') + end + end + + describe '#read_offset_and_length' do + context 'without the chat_reply trace section' do + it 'falls back to using the build_script trace section' do + expect(output) + .to receive(:find_build_trace_section) + .with('chat_reply') + .and_return(nil) + + expect(output) + .to receive(:find_build_trace_section) + .with('build_script') + .and_return({ name: 'build_script', byte_start: 1, byte_end: 4 }) + + expect(output.read_offset_and_length).to eq([1, 3]) + end + end + + context 'without the build_script trace section' do + it 'raises MissingBuildSectionError' do + expect { output.read_offset_and_length } + .to raise_error(described_class::MissingBuildSectionError) + end + end + + context 'with the chat_reply trace section' do + it 'returns the read offset and length as an Array' do + trace = Gitlab::Ci::Trace.new(build) + + allow(build) + .to receive(:trace) + .and_return(trace) + + allow(trace) + .to receive(:extract_sections) + .and_return([{ name: 'chat_reply', byte_start: 1, byte_end: 4 }]) + + expect(output.read_offset_and_length).to eq([1, 3]) + end + end + end + + describe '#without_executed_command_line' do + it 'returns the input without the first line' do + expect(output.without_executed_command_line("hello\nworld")) + .to eq('world') + end + + it 'returns an empty String when the input is empty' do + expect(output.without_executed_command_line('')).to eq('') + end + + it 'returns an empty String when the input consits of a single newline' do + expect(output.without_executed_command_line("\n")).to eq('') + end + end + + describe '#find_build_trace_section' do + it 'returns nil when no section could be found' do + expect(output.find_build_trace_section('foo')).to be_nil + end + + it 'returns the trace section when it could be found' do + section = { name: 'chat_reply', byte_start: 1, byte_end: 4 } + + allow(output) + .to receive(:trace_sections) + .and_return([section]) + + expect(output.find_build_trace_section('chat_reply')).to eq(section) + end + end +end diff --git a/spec/lib/gitlab/chat/responder/base_spec.rb b/spec/lib/gitlab/chat/responder/base_spec.rb new file mode 100644 index 00000000000..7fa9bad9d38 --- /dev/null +++ b/spec/lib/gitlab/chat/responder/base_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder::Base do + let(:project) { double(:project) } + let(:pipeline) { double(:pipeline, project: project) } + let(:build) { double(:build, pipeline: pipeline) } + let(:responder) { described_class.new(build) } + + describe '#pipeline' do + it 'returns the pipeline' do + expect(responder.pipeline).to eq(pipeline) + end + end + + describe '#project' do + it 'returns the project' do + expect(responder.project).to eq(project) + end + end + + describe '#success' do + it 'raises NotImplementedError' do + expect { responder.success }.to raise_error(NotImplementedError) + end + end + + describe '#failure' do + it 'raises NotImplementedError' do + expect { responder.failure }.to raise_error(NotImplementedError) + end + end + + describe '#send_response' do + it 'raises NotImplementedError' do + expect { responder.send_response('hello') } + .to raise_error(NotImplementedError) + end + end + + describe '#scheduled_output' do + it 'raises NotImplementedError' do + expect { responder.scheduled_output } + .to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/chat/responder/slack_spec.rb b/spec/lib/gitlab/chat/responder/slack_spec.rb new file mode 100644 index 00000000000..a1553232b32 --- /dev/null +++ b/spec/lib/gitlab/chat/responder/slack_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder::Slack do + let(:chat_name) { create(:chat_name, chat_id: 'U123') } + + let(:pipeline) do + pipeline = create(:ci_pipeline) + + pipeline.create_chat_data!( + response_url: 'http://example.com', + chat_name_id: chat_name.id + ) + + pipeline + end + + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:responder) { described_class.new(build) } + + describe '#send_response' do + it 'sends a response back to Slack' do + expect(Gitlab::HTTP).to receive(:post).with( + 'http://example.com', + { headers: { Accept: 'application/json' }, body: 'hello'.to_json } + ) + + responder.send_response('hello') + end + end + + describe '#success' do + it 'returns the output for a successful build' do + expect(responder) + .to receive(:send_response) + .with(hash_including(text: /<@U123>:.+hello/, response_type: :in_channel)) + + responder.success('hello') + end + + it 'limits the output to a fixed size' do + expect(responder) + .to receive(:send_response) + .with(hash_including(text: /The output is too large/)) + + responder.success('a' * 4000) + end + + it 'does not send a response if the output is empty' do + expect(responder).not_to receive(:send_response) + + responder.success('') + end + end + + describe '#failure' do + it 'returns the output for a failed build' do + expect(responder).to receive(:send_response).with( + hash_including( + text: /<@U123>:.+Sorry, the build failed!/, + response_type: :in_channel + ) + ) + + responder.failure + end + end + + describe '#scheduled_output' do + it 'returns the output for a scheduled build' do + output = responder.scheduled_output + + expect(output).to eq({ text: '' }) + end + end +end diff --git a/spec/lib/gitlab/chat/responder_spec.rb b/spec/lib/gitlab/chat/responder_spec.rb new file mode 100644 index 00000000000..9893689cba9 --- /dev/null +++ b/spec/lib/gitlab/chat/responder_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder do + describe '.responder_for' do + context 'using a regular build' do + it 'returns nil' do + build = create(:ci_build) + + expect(described_class.responder_for(build)).to be_nil + end + end + + context 'using a chat build' do + it 'returns the responder for the build' do + pipeline = create(:ci_pipeline) + build = create(:ci_build, pipeline: pipeline) + service = double(:service, chat_responder: Gitlab::Chat::Responder::Slack) + chat_name = double(:chat_name, service: service) + chat_data = double(:chat_data, chat_name: chat_name) + + allow(pipeline) + .to receive(:chat_data) + .and_return(chat_data) + + expect(described_class.responder_for(build)) + .to be_an_instance_of(Gitlab::Chat::Responder::Slack) + end + end + end +end diff --git a/spec/lib/gitlab/chat_spec.rb b/spec/lib/gitlab/chat_spec.rb new file mode 100644 index 00000000000..d61c4b36668 --- /dev/null +++ b/spec/lib/gitlab/chat_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Chat, :use_clean_rails_memory_store_caching do + describe '.available?' do + it 'returns true when the chatops feature is available' do + allow(Feature) + .to receive(:enabled?) + .with(:chatops, default_enabled: true) + .and_return(true) + + expect(described_class).to be_available + end + + it 'returns false when the chatops feature is not available' do + allow(Feature) + .to receive(:enabled?) + .with(:chatops, default_enabled: true) + .and_return(false) + + expect(described_class).not_to be_available + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb new file mode 100644 index 00000000000..7c1c016b4bb --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do + let(:project) { create(:project, :repository) } + + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project, ref: 'master') + end + + let(:command) do + double(:command, project: project, chat_data: { command: 'echo' }) + end + + describe '#perform!' do + it 'removes unwanted jobs for chat pipelines' do + allow(pipeline).to receive(:chat?).and_return(true) + + pipeline.config_processor.jobs[:echo] = double(:job) + + described_class.new(pipeline, command).perform! + + expect(pipeline.config_processor.jobs.keys).to eq([:echo]) + end + end + + it 'does not remove any jobs for non-chat pipelines' do + described_class.new(pipeline, command).perform! + + expect(pipeline.config_processor.jobs.keys).to eq([:rspec]) + end +end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 793cac593a2..00cb1e6446a 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -217,6 +217,7 @@ describe Gitlab::Danger::Helper do 'app/views/foo' | :frontend 'public/foo' | :frontend 'spec/javascripts/foo' | :frontend + 'spec/frontend/bar' | :frontend 'vendor/assets/foo' | :frontend 'jest.config.js' | :frontend 'package.json' | :frontend @@ -225,6 +226,7 @@ describe Gitlab::Danger::Helper do 'ee/app/assets/foo' | :frontend 'ee/app/views/foo' | :frontend 'ee/spec/javascripts/foo' | :frontend + 'ee/spec/frontend/bar' | :frontend 'app/models/foo' | :backend 'bin/foo' | :backend diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c15b360b563..f2eccec4635 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -131,6 +131,7 @@ ci_pipelines: - merge_request - deployments - environments +- chat_data pipeline_variables: - pipeline stages: diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb new file mode 100644 index 00000000000..b203a1ee79c --- /dev/null +++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::ApplicationHelp do + let(:params) { { command: '/gitlab', text: 'help' } } + + describe '#execute' do + subject do + described_class.new(params).execute + end + + it 'displays the help section' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to include('Available commands') + expect(subject[:text]).to include('/gitlab [project name or alias] issue show') + end + end +end diff --git a/spec/lib/gitlab/slash_commands/presenters/error_spec.rb b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb new file mode 100644 index 00000000000..30ff81510c1 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Presenters::Error do + subject { described_class.new('Error').message } + + it { is_expected.to be_a(Hash) } + + it 'shows the error message' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:status]).to eq(200) + expect(subject[:text]).to eq('Error') + end +end diff --git a/spec/lib/gitlab/slash_commands/presenters/run_spec.rb b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb new file mode 100644 index 00000000000..f3ab01ef6bb --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Presenters::Run do + let(:presenter) { described_class.new } + + describe '#present' do + context 'when no builds are present' do + it 'returns an error' do + builds = double(:builds, take: nil) + pipeline = double(:pipeline, builds: builds) + + expect(presenter) + .to receive(:unsupported_chat_service) + + presenter.present(pipeline) + end + end + + context 'when a responder could be found' do + it 'returns the output for a scheduled pipeline' do + responder = double(:responder, scheduled_output: 'hello') + build = double(:build) + builds = double(:builds, take: build) + pipeline = double(:pipeline, builds: builds) + + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(responder) + + expect(presenter) + .to receive(:in_channel_response) + .with('hello') + + presenter.present(pipeline) + end + end + + context 'when a responder could not be found' do + it 'returns an error' do + build = double(:build) + builds = double(:builds, take: build) + pipeline = double(:pipeline, builds: builds) + + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(nil) + + expect(presenter) + .to receive(:unsupported_chat_service) + + presenter.present(pipeline) + end + end + end + + describe '#unsupported_chat_service' do + it 'returns an ephemeral response' do + expect(presenter) + .to receive(:ephemeral_response) + .with(text: /Sorry, this chat service is currently not supported/) + + presenter.unsupported_chat_service + end + end + + describe '#failed_to_schedule' do + it 'returns an ephemeral response' do + expect(presenter) + .to receive(:ephemeral_response) + .with(text: /The command could not be scheduled/) + + presenter.failed_to_schedule('foo') + end + end +end diff --git a/spec/lib/gitlab/slash_commands/run_spec.rb b/spec/lib/gitlab/slash_commands/run_spec.rb new file mode 100644 index 00000000000..900fae05719 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/run_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Run do + describe '.available?' do + it 'returns true when builds are enabled for the project' do + project = double(:project, builds_enabled?: true) + + allow(Gitlab::Chat) + .to receive(:available?) + .and_return(true) + + expect(described_class.available?(project)).to eq(true) + end + + it 'returns false when builds are disabled for the project' do + project = double(:project, builds_enabled?: false) + + expect(described_class.available?(project)).to eq(false) + end + + it 'returns false when chatops is not available' do + allow(Gitlab::Chat) + .to receive(:available?) + .and_return(false) + + project = double(:project, builds_enabled?: true) + + expect(described_class.available?(project)).to eq(false) + end + end + + describe '.allowed?' do + it 'returns true when the user can create a pipeline' do + project = create(:project) + + expect(described_class.allowed?(project, project.creator)).to eq(true) + end + + it 'returns false when the user can not create a pipeline' do + project = create(:project) + user = create(:user) + + expect(described_class.allowed?(project, user)).to eq(false) + end + end + + describe '#execute' do + let(:chat_name) { create(:chat_name) } + let(:project) { create(:project) } + + let(:command) do + described_class.new(project, chat_name, response_url: 'http://example.com') + end + + context 'when a pipeline could not be scheduled' do + it 'returns an error' do + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(nil) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:failed_to_schedule) + .with('foo') + + command.execute(command: 'foo', arguments: '') + end + end + + context 'when a pipeline could be created but the chat service was not supported' do + it 'returns an error' do + build = double(:build) + pipeline = double( + :pipeline, + builds: double(:relation, take: build), + persisted?: true + ) + + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(pipeline) + + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(nil) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:unsupported_chat_service) + + command.execute(command: 'foo', arguments: '') + end + end + + context 'using a valid pipeline' do + it 'schedules the pipeline' do + responder = double(:responder, scheduled_output: 'hello') + build = double(:build) + pipeline = double( + :pipeline, + builds: double(:relation, take: build), + persisted?: true + ) + + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(pipeline) + + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(responder) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:in_channel_response) + .with(responder.scheduled_output) + + command.execute(command: 'foo', arguments: '') + end + end + end +end diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index 6fbf60a6222..88e7e2e5ebb 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -36,7 +36,7 @@ describe Sentry::Client do end it 'does not follow redirects' do - expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') + expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302') expect(redirect_req_stub).to have_been_requested expect(redirected_req_stub).not_to have_been_requested end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 460b5c8cd31..b9567ab4d65 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -22,6 +22,7 @@ describe Ci::Pipeline, :mailer do it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } + it { is_expected.to have_one(:chat_data) } it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:status) } diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index d30228b863c..076ccc96041 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -145,6 +145,24 @@ describe ErrorTracking::ProjectErrorTrackingSetting do expect(result).to be_nil end end + + context 'when sentry client raises exception' do + let(:sentry_client) { spy(:sentry_client) } + + before do + synchronous_reactive_cache(subject) + + allow(subject).to receive(:sentry_client).and_return(sentry_client) + allow(sentry_client).to receive(:list_issues).with(opts) + .and_raise(Sentry::Client::Error, 'error message') + end + + it 'returns error' do + expect(result).to eq(error: 'error message') + expect(subject).to have_received(:sentry_client) + expect(sentry_client).to have_received(:list_issues) + end + end end describe '#list_sentry_projects' do diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb index 0d95f454819..5c4bce90ace 100644 --- a/spec/models/project_services/slack_slash_commands_service_spec.rb +++ b/spec/models/project_services/slack_slash_commands_service_spec.rb @@ -38,4 +38,11 @@ describe SlackSlashCommandsService do end end end + + describe '#chat_responder' do + it 'returns the responder to use for Slack' do + expect(described_class.new.chat_responder) + .to eq(Gitlab::Chat::Responder::Slack) + end + end end diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb index d9dab1d705c..9d4fc62f923 100644 --- a/spec/services/error_tracking/list_issues_service_spec.rb +++ b/spec/services/error_tracking/list_issues_service_spec.rb @@ -45,7 +45,23 @@ describe ErrorTracking::ListIssuesService do it 'result is not ready' do expect(result).to eq( - status: :error, http_status: :no_content, message: 'not ready') + status: :error, http_status: :no_content, message: 'Not ready. Try again later') + end + end + + context 'when list_sentry_issues returns error' do + before do + allow(error_tracking_setting) + .to receive(:list_sentry_issues) + .and_return(error: 'Sentry response status code: 401') + end + + it 'returns the error' do + expect(result).to eq( + status: :error, + http_status: :bad_request, + message: 'Sentry response status code: 401' + ) end end end @@ -58,7 +74,11 @@ describe ErrorTracking::ListIssuesService do it 'returns error' do result = subject.execute - expect(result).to include(status: :error, message: 'access denied') + expect(result).to include( + status: :error, + message: 'Access denied', + http_status: :unauthorized + ) end end @@ -70,7 +90,7 @@ describe ErrorTracking::ListIssuesService do it 'raises error' do result = subject.execute - expect(result).to include(status: :error, message: 'not enabled') + expect(result).to include(status: :error, message: 'Error Tracking is not enabled') end end end diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb index ee9c59e3f65..9f25a633deb 100644 --- a/spec/services/error_tracking/list_projects_service_spec.rb +++ b/spec/services/error_tracking/list_projects_service_spec.rb @@ -53,11 +53,11 @@ describe ErrorTracking::ListProjectsService do context 'sentry client raises exception' do before do expect(error_tracking_setting).to receive(:list_sentry_projects) - .and_raise(Sentry::Client::Error, 'Sentry response error: 500') + .and_raise(Sentry::Client::Error, 'Sentry response status code: 500') end it 'returns error response' do - expect(result[:message]).to eq('Sentry response error: 500') + expect(result[:message]).to eq('Sentry response status code: 500') expect(result[:http_status]).to eq(:bad_request) end end diff --git a/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb new file mode 100644 index 00000000000..713f0a879c1 --- /dev/null +++ b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'issuable state' do + it 'exposes all the existing issuable states' do + expect(described_class.values.keys).to include(*%w[opened closed locked]) + end +end diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb index ff88efd0e31..1d9c6d36ad7 100644 --- a/spec/views/projects/issues/show.html.haml_spec.rb +++ b/spec/views/projects/issues/show.html.haml_spec.rb @@ -21,12 +21,24 @@ describe 'projects/issues/show' do allow(issue).to receive(:closed?).and_return(true) end - it 'shows "Closed (moved)" if an issue has been moved' do - allow(issue).to receive(:moved?).and_return(true) + context 'when the issue was moved' do + let(:new_issue) { create(:issue, project: project, author: user) } - render + before do + issue.moved_to = new_issue + end + + it 'shows "Closed (moved)" if an issue has been moved' do + render + + expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + end + + it 'links "moved" to the new issue the original issue was moved to' do + render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved') + end end it 'shows "Closed" if an issue has not been moved' do diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index acd8da11d8d..ccb26849e67 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -26,5 +26,24 @@ describe BuildFinishedWorker do .not_to raise_error end end + + it 'schedules a ChatNotification job for a chat build' do + build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) + + expect(ChatNotificationWorker) + .to receive(:perform_async) + .with(build.id) + + described_class.new.perform(build.id) + end + + it 'does not schedule a ChatNotification job for a regular build' do + build = create(:ci_build, :success, pipeline: create(:ci_pipeline)) + + expect(ChatNotificationWorker) + .not_to receive(:perform_async) + + described_class.new.perform(build.id) + end end end diff --git a/spec/workers/chat_notification_worker_spec.rb b/spec/workers/chat_notification_worker_spec.rb new file mode 100644 index 00000000000..91695674f5d --- /dev/null +++ b/spec/workers/chat_notification_worker_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ChatNotificationWorker do + let(:worker) { described_class.new } + let(:chat_build) do + create(:ci_build, pipeline: create(:ci_pipeline, source: :chat)) + end + + describe '#perform' do + it 'does nothing when the build no longer exists' do + expect(worker).not_to receive(:send_response) + + worker.perform(-1) + end + + it 'sends a response for an existing build' do + expect(worker) + .to receive(:send_response) + .with(an_instance_of(Ci::Build)) + + worker.perform(chat_build.id) + end + + it 'reschedules the job if the trace sections could not be found' do + expect(worker) + .to receive(:send_response) + .and_raise(Gitlab::Chat::Output::MissingBuildSectionError) + + expect(described_class) + .to receive(:perform_in) + .with(described_class::RESCHEDULE_INTERVAL, chat_build.id) + + worker.perform(chat_build.id) + end + end + + describe '#send_response' do + context 'when a responder could not be found' do + it 'does nothing' do + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(chat_build) + .and_return(nil) + + expect(worker.send_response(chat_build)).to be_nil + end + end + + context 'when a responder could be found' do + let(:responder) { double(:responder) } + + before do + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(chat_build) + .and_return(responder) + end + + it 'sends the response for a succeeded build' do + output = double(:output, to_s: 'this is the build output') + + expect(chat_build) + .to receive(:success?) + .and_return(true) + + expect(responder) + .to receive(:success) + .with(an_instance_of(String)) + + expect(Gitlab::Chat::Output) + .to receive(:new) + .with(chat_build) + .and_return(output) + + worker.send_response(chat_build) + end + + it 'sends the response for a failed build' do + expect(chat_build) + .to receive(:success?) + .and_return(false) + + expect(responder).to receive(:failure) + + worker.send_response(chat_build) + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 0111b583404..9ecb2b82f8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -653,10 +653,10 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.52.0": - version "1.52.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.52.0.tgz#870b0112a18b10d2cde5470a48b05025193cd207" - integrity sha512-xqDYIaSY1MJuCa7lRIDTTPoXn6x57So2qchxwmELE+SAJxlYlpYgDKCNWcGawhyTZRDZuG/qFBWp0sMeTQD//A== +"@gitlab/svgs@^1.53.0": + version "1.53.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.53.0.tgz#a0c463953f24ff07a5871a68d3e9ae3cb45e5267" + integrity sha512-u4AqIKCVcnG727BLW6wxRkXDfDexD4KlT2JxiPtKBJLmA6mwhSiCCD3ZDMd5W+bbTfl48KdcuV03MXmiqyONNw== "@gitlab/ui@^2.0.4": version "2.0.4" |