Commit 5e11c9b7 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 97d4d926
.notify: .notify:
image: alpine image: ruby:2.6-alpine
stage: notification stage: notification
dependencies: [] dependencies: []
cache: {} cache: {}
before_script: before_script:
- apk update && apk add git curl bash - apk update && apk add git curl bash
- source scripts/utils.sh
- source scripts/notifications.sh
- install_gitlab_gem
variables: variables:
COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list" COMMIT_NOTES_URL: "https://${CI_SERVER_HOST}/${CI_PROJECT_PATH}/commit/${CI_COMMIT_SHA}#notes-list"
schedule:package-and-qa:notify-failure: schedule:package-and-qa:notify-failure:
extends: extends:
- .only:variables_refs-canonical-dot-com-schedules - .only:variables_refs-canonical-dot-com-schedules
- .notify - .notify
script: script:
- 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_failing' - 'export NOTIFICATION_MESSAGE=":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See ${CI_PIPELINE_URL}. For downstream pipelines, see ${COMMIT_NOTES_URL}"'
- 'notify_on_job_failure schedule:package-and-qa qa-master "${NOTIFICATION_MESSAGE}" ci_failing'
needs: ["schedule:package-and-qa"] needs: ["schedule:package-and-qa"]
when: on_failure allow_failure: true
when: always
## What is the productivity problem to solve?
<!--
Please describe the productivity problem that needs to be solved backed by charts from
https://about.gitlab.com/handbook/engineering/quality/engineering-productivity-team/#engineering-productivity-team-metrics.
-->
### Problem identification checklist
- [ ] The root cause of the problem is identified.
- [ ] The surface of the problem is as small as possible.
## What are the potential solutions?
<!--
Please provide potential solutions here. Example solutions could be:
- Dogfood a feature.
- Refactor/improve some workflow code.
- Throw more money at the problem.
Please provide pros/cons and a weight estimate for each solution.
-->
- [ ] All potential solutions are listed.
- [ ] A solution has been chosen for the first iteration: `PUT THE CHOSEN SOLUTION HERE`
## Who and when will the solution be implemented?
<!--
For history reason, please list the person that will implement the solution and
the planned milestone/date.
-->
## Verify that the solution has improved the situation
<!--
Ideally, looking at the charts from the first part, we should see an improvement
after the implementation is merged/deployed/released.
-->
- [ ] The solution improved the situation.
- If yes, check this box and close the issue. Well done! :tada:
- Otherwise, create a new "Productivity Improvement" issue. You can re-use the description from this issue, but obviously another solution should be chosen this time.
/label ~"Engineering Productivity" ~meta
/cc @gl-quality/eng-prod
...@@ -127,9 +127,11 @@ export default { ...@@ -127,9 +127,11 @@ export default {
<input name="issue[description]" :value="issueDescription" type="hidden" /> <input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" /> <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
<loading-button <loading-button
v-if="!error.gitlab_issue"
class="btn-success" class="btn-success"
:label="__('Create issue')" :label="__('Create issue')"
:loading="issueCreationInProgress" :loading="issueCreationInProgress"
data-qa-selector="create_issue_button"
@click="createIssue" @click="createIssue"
/> />
</form> </form>
...@@ -140,6 +142,12 @@ export default { ...@@ -140,6 +142,12 @@ export default {
</tooltip-on-truncate> </tooltip-on-truncate>
<h3>{{ __('Error details') }}</h3> <h3>{{ __('Error details') }}</h3>
<ul> <ul>
<li v-if="error.gitlab_issue">
<span class="bold">{{ __('GitLab Issue') }}:</span>
<gl-link :href="error.gitlab_issue">
<span>{{ error.gitlab_issue }}</span>
</gl-link>
</li>
<li> <li>
<span class="bold">{{ __('Sentry event') }}:</span> <span class="bold">{{ __('Sentry event') }}:</span>
<gl-link <gl-link
......
...@@ -42,6 +42,7 @@ class Issue < ApplicationRecord ...@@ -42,6 +42,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings has_many :zoom_meetings
has_one :sentry_issue
validates :project, presence: true validates :project, presence: true
......
# frozen_string_literal: true
class SentryIssue < ApplicationRecord
belongs_to :issue
validates :issue, uniqueness: true, presence: true
validates :sentry_issue_identifier, presence: true
end
...@@ -8,6 +8,18 @@ class Timelog < ApplicationRecord ...@@ -8,6 +8,18 @@ class Timelog < ApplicationRecord
belongs_to :merge_request, touch: true belongs_to :merge_request, touch: true
belongs_to :user belongs_to :user
scope :for_issues_in_group, -> (group) do
joins(:issue).where(
'EXISTS (?)',
Project.select(1).where(namespace: group.self_and_descendants)
.where('issues.project_id = projects.id')
)
end
scope :between_dates, -> (start_date, end_date) do
where('spent_at BETWEEN ? AND ?', start_date, end_date)
end
def issuable def issuable
issue || merge_request issue || merge_request
end end
......
...@@ -10,6 +10,7 @@ module ErrorTracking ...@@ -10,6 +10,7 @@ module ErrorTracking
:first_release_short_version, :first_release_short_version,
:first_seen, :first_seen,
:frequency, :frequency,
:gitlab_issue,
:id, :id,
:last_release_last_commit, :last_release_last_commit,
:last_release_short_version, :last_release_short_version,
......
---
title: Add SentryIssue table to store a link between issue and sentry issue
merge_request: 37026
author:
type: added
---
title: Increase lower DAG `needs` limit from five to ten
merge_request: 21237
author:
type: changed
---
title: Fix misaligned approval tr
merge_request: 21368
author: Lee Tickett
type: fixed
---
title: Surface GitLab issue in error detail page
merge_request: 21019
author:
type: added
...@@ -29,8 +29,8 @@ def ce_port_changelog?(changelog_path) ...@@ -29,8 +29,8 @@ def ce_port_changelog?(changelog_path)
helper.ee? && !ee_changelog?(changelog_path) helper.ee? && !ee_changelog?(changelog_path)
end end
def docs_only_change? def categories_need_changelog?
helper.changes_by_category.keys == [:docs] (helper.changes_by_category.keys - %i[docs none]).any?
end end
def check_changelog(path) def check_changelog(path)
...@@ -59,7 +59,7 @@ def sanitized_mr_title ...@@ -59,7 +59,7 @@ def sanitized_mr_title
gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`') gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
end end
changelog_needed = !docs_only_change? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? changelog_needed = categories_need_changelog? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
if git.modified_files.include?("CHANGELOG.md") if git.modified_files.include?("CHANGELOG.md")
......
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddTimelogSpentAtIndex < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :timelogs, :spent_at, where: 'spent_at IS NOT NULL'
end
def down
remove_concurrent_index :timelogs, :spent_at, where: 'spent_at IS NOT NULL'
end
end
# frozen_string_literal: true
class CreateSentryIssuesTable < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :sentry_issues do |t|
t.references :issue,
foreign_key: { on_delete: :cascade },
index: { unique: true },
null: false
t.bigint :sentry_issue_identifier, null: false
end
end
end
...@@ -3607,6 +3607,12 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do ...@@ -3607,6 +3607,12 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
t.index ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true t.index ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true
end end
create_table "sentry_issues", force: :cascade do |t|
t.bigint "issue_id", null: false
t.bigint "sentry_issue_identifier", null: false
t.index ["issue_id"], name: "index_sentry_issues_on_issue_id", unique: true
end
create_table "service_desk_settings", primary_key: "project_id", id: :bigint, default: nil, force: :cascade do |t| create_table "service_desk_settings", primary_key: "project_id", id: :bigint, default: nil, force: :cascade do |t|
t.string "issue_template_key", limit: 255 t.string "issue_template_key", limit: 255
end end
...@@ -3812,6 +3818,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do ...@@ -3812,6 +3818,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
t.datetime "spent_at" t.datetime "spent_at"
t.index ["issue_id"], name: "index_timelogs_on_issue_id" t.index ["issue_id"], name: "index_timelogs_on_issue_id"
t.index ["merge_request_id"], name: "index_timelogs_on_merge_request_id" t.index ["merge_request_id"], name: "index_timelogs_on_merge_request_id"
t.index ["spent_at"], name: "index_timelogs_on_spent_at", where: "(spent_at IS NOT NULL)"
t.index ["user_id"], name: "index_timelogs_on_user_id" t.index ["user_id"], name: "index_timelogs_on_user_id"
end end
...@@ -4660,6 +4667,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do ...@@ -4660,6 +4667,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
add_foreign_key "scim_oauth_access_tokens", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "scim_oauth_access_tokens", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "self_managed_prometheus_alert_events", "environments", on_delete: :cascade add_foreign_key "self_managed_prometheus_alert_events", "environments", on_delete: :cascade
add_foreign_key "self_managed_prometheus_alert_events", "projects", on_delete: :cascade add_foreign_key "self_managed_prometheus_alert_events", "projects", on_delete: :cascade
add_foreign_key "sentry_issues", "issues", on_delete: :cascade
add_foreign_key "service_desk_settings", "projects", on_delete: :cascade add_foreign_key "service_desk_settings", "projects", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade add_foreign_key "slack_integrations", "services", on_delete: :cascade
......
...@@ -2156,6 +2156,10 @@ type Group { ...@@ -2156,6 +2156,10 @@ type Group {
""" """
state: EpicState state: EpicState
): EpicConnection ): EpicConnection
"""
Indicates if Epics are enabled for namespace
"""
epicsEnabled: Boolean epicsEnabled: Boolean
""" """
...@@ -2168,6 +2172,11 @@ type Group { ...@@ -2168,6 +2172,11 @@ type Group {
""" """
fullPath: ID! fullPath: ID!
"""
Indicates if Group timelogs are enabled for namespace
"""
groupTimelogsEnabled: Boolean
""" """
ID of the namespace ID of the namespace
""" """
...@@ -2233,6 +2242,41 @@ type Group { ...@@ -2233,6 +2242,41 @@ type Group {
""" """
rootStorageStatistics: RootStorageStatistics rootStorageStatistics: RootStorageStatistics
"""
Time logged in issues by group members
"""
timelogs(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
List time logs within a time range where the logged date is before end_date parameter.
"""
endDate: Time!
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
List time logs within a time range where the logged date is after start_date parameter.
"""
startDate: Time!
): TimelogConnection!
""" """
Permissions for the current user on the resource Permissions for the current user on the resource
""" """
...@@ -5484,6 +5528,63 @@ Time represented in ISO 8601 ...@@ -5484,6 +5528,63 @@ Time represented in ISO 8601
""" """
scalar Time scalar Time
type Timelog {
"""
The date when the time tracked was spent at
"""
date: Time!
"""
The issue that logged time was added to
"""
issue: Issue
"""
The time spent displayed in seconds
"""
timeSpent: Int!
"""
The user that logged the time
"""
user: User!
}
"""
The connection type for Timelog.
"""
type TimelogConnection {
"""
A list of edges.
"""
edges: [TimelogEdge]
"""
A list of nodes.
"""
nodes: [Timelog]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type TimelogEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Timelog
}
""" """
Representing a todo entry Representing a todo entry
""" """
......
...@@ -3223,7 +3223,7 @@ ...@@ -3223,7 +3223,7 @@
}, },
{ {
"name": "epicsEnabled", "name": "epicsEnabled",
"description": null, "description": "Indicates if Epics are enabled for namespace",
"args": [ "args": [
], ],
...@@ -3271,6 +3271,20 @@ ...@@ -3271,6 +3271,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "groupTimelogsEnabled",
"description": "Indicates if Group timelogs are enabled for namespace",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "id", "name": "id",
"description": "ID of the namespace", "description": "ID of the namespace",
...@@ -3448,6 +3462,91 @@ ...@@ -3448,6 +3462,91 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "timelogs",
"description": "Time logged in issues by group members",
"args": [
{
"name": "startDate",
"description": "List time logs within a time range where the logged date is after start_date parameter.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "endDate",
"description": "List time logs within a time range where the logged date is before end_date parameter.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "TimelogConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "userPermissions", "name": "userPermissions",
"description": "Permissions for the current user on the resource", "description": "Permissions for the current user on the resource",
...@@ -10813,6 +10912,199 @@ ...@@ -10813,6 +10912,199 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "TimelogConnection",
"description": "The connection type for Timelog.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "TimelogEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Timelog",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TimelogEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Timelog",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Timelog",
"description": null,
"fields": [
{
"name": "date",
"description": "The date when the time tracked was spent at",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issue",
"description": "The issue that logged time was added to",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "timeSpent",
"description": "The time spent displayed in seconds",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "user",
"description": "The user that logged the time",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ProjectStatistics", "name": "ProjectStatistics",
......
...@@ -320,7 +320,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -320,7 +320,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `webUrl` | String! | Web URL of the group | | `webUrl` | String! | Web URL of the group |
| `avatarUrl` | String | Avatar URL of the group | | `avatarUrl` | String | Avatar URL of the group |
| `parent` | Group | Parent group | | `parent` | Group | Parent group |
| `epicsEnabled` | Boolean | | | `epicsEnabled` | Boolean | Indicates if Epics are enabled for namespace |
| `groupTimelogsEnabled` | Boolean | Indicates if Group timelogs are enabled for namespace |
| `epic` | Epic | | | `epic` | Epic | |
### GroupPermissions ### GroupPermissions
...@@ -836,6 +837,15 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -836,6 +837,15 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `count` | Int! | Number of total tasks | | `count` | Int! | Number of total tasks |
| `completedCount` | Int! | Number of completed tasks | | `completedCount` | Int! | Number of completed tasks |
### Timelog
| Name | Type | Description |
| --- | ---- | ---------- |
| `date` | Time! | The date when the time tracked was spent at |
| `timeSpent` | Int! | The time spent displayed in seconds |
| `user` | User! | The user that logged the time |
| `issue` | Issue | The issue that logged time was added to |
### Todo ### Todo
| Name | Type | Description | | Name | Type | Description |
......
...@@ -2256,11 +2256,11 @@ This example creates three paths of execution: ...@@ -2256,11 +2256,11 @@ This example creates three paths of execution:
pipeline will be created with YAML error. pipeline will be created with YAML error.
- We are temporarily limiting the maximum number of jobs that a single job can - We are temporarily limiting the maximum number of jobs that a single job can
need in the `needs:` array: need in the `needs:` array:
- For GitLab.com, the limit is five. For more information, see our - For GitLab.com, the limit is ten. For more information, see our
[infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541). [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541).
- For self-managed instances, the limit is: - For self-managed instances, the limit is:
- Five by default (`ci_dag_limit_needs` feature flag is enabled). - 10, if the `ci_dag_limit_needs` feature flag is enabled (default).
- 50 if the `ci_dag_limit_needs` feature flag is disabled. - 50, if the `ci_dag_limit_needs` feature flag is disabled.
- It is impossible for now to have `needs: []` (empty needs), the job always needs to - It is impossible for now to have `needs: []` (empty needs), the job always needs to
depend on something, unless this is the job in the first stage. However, support for depend on something, unless this is the job in the first stage. However, support for
an empty needs array [is planned](https://gitlab.com/gitlab-org/gitlab/issues/30631). an empty needs array [is planned](https://gitlab.com/gitlab-org/gitlab/issues/30631).
......
...@@ -38,7 +38,7 @@ Feature.enabled?(:feature_flag, project, default_enabled: true) ...@@ -38,7 +38,7 @@ Feature.enabled?(:feature_flag, project, default_enabled: true)
The [`Project#feature_available?`][project-fa], The [`Project#feature_available?`][project-fa],
[`Namespace#feature_available?`][namespace-fa] (EE), and [`Namespace#feature_available?`][namespace-fa] (EE), and
[`License.feature_available?`][license-fa] (EE) methods all implicitly check for [`License.feature_available?`][license-fa] (EE) methods all implicitly check for
a feature flag by the same name as the provided argument. a by default enabled feature flag with the same name as the provided argument.
For example if a feature is license-gated, there's no need to add an additional For example if a feature is license-gated, there's no need to add an additional
explicit feature flag check since the flag will be checked as part of the explicit feature flag check since the flag will be checked as part of the
...@@ -56,12 +56,19 @@ isn't gated by a License or Plan. ...@@ -56,12 +56,19 @@ isn't gated by a License or Plan.
unless the feature is explicitly disabled or limited to a percentage of users, unless the feature is explicitly disabled or limited to a percentage of users,
the feature flag check will default to `true`.** the feature flag check will default to `true`.**
As an example, if you were to ship the backend half of a feature behind a flag, This is relevant when developing the feature using
you'd want to explicitly disable that flag until the frontend half is also ready [several smaller merge requests](https://about.gitlab.com/handbook/values/#make-small-merge-requests), or when the feature is considered to be an
to be shipped. To make sure this feature is disabled for both GitLab.com and [alpha or beta](https://about.gitlab.com/handbook/product/#alpha-beta-ga), and
self-managed instances you'd need to explicitly call `Feature.enabled?` method should not be available by default.
before the `feature_available` method. This ensures the feature_flag is defaulting
to `false`. As an example, if you were to ship the frontend half of a feature without the
backend, you'd want to disable the feature entirely until the backend half is
also ready to be shipped. To make sure this feature is disabled for both
GitLab.com and self-managed instances, you should use the
[`Namespace#alpha_feature_available?`](https://gitlab.com/gitlab-org/gitlab/blob/458749872f4a8f27abe8add930dbb958044cb926/ee/app/models/ee/namespace.rb#L113) or
[`Namespace#beta_feature_available?`](https://gitlab.com/gitlab-org/gitlab/blob/458749872f4a8f27abe8add930dbb958044cb926/ee/app/models/ee/namespace.rb#L100-112)
method, according to our [definitions](https://about.gitlab.com/handbook/product/#alpha-beta-ga). This ensures the feature is disabled unless the feature flag is
_explicitly_ enabled.
## Feature groups ## Feature groups
......
...@@ -208,7 +208,7 @@ and it is useful for knowing which versions won't be compatible between them. ...@@ -208,7 +208,7 @@ and it is useful for knowing which versions won't be compatible between them.
### When to bump the version up ### When to bump the version up
We will have to bump the verision if we rename model/columns or perform any format We will have to bump the version if we rename model/columns or perform any format
modifications in the JSON structure or the file structure of the archive file. modifications in the JSON structure or the file structure of the archive file.
We do not need to bump the version up in any of the following cases: We do not need to bump the version up in any of the following cases:
......
...@@ -133,6 +133,8 @@ CHROME_HEADLESS=0 bundle exec rspec some_spec.rb ...@@ -133,6 +133,8 @@ CHROME_HEADLESS=0 bundle exec rspec some_spec.rb
``` ```
The test will go by quickly, but this will give you an idea of what's happening. The test will go by quickly, but this will give you an idea of what's happening.
Using `live_debug` with `CHROME_HEADLESS=0` pauses the open browser, and does not
open the page again. This can be used to debug and inspect elements.
You can also add `byebug` or `binding.pry` to pause execution and [step through](../pry_debugging.md#stepping) You can also add `byebug` or `binding.pry` to pause execution and [step through](../pry_debugging.md#stepping)
the test. the test.
......
...@@ -128,9 +128,16 @@ automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.m ...@@ -128,9 +128,16 @@ automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.m
need to explicitly provide the `KUBE_NAMESPACE` [deployment variable](#deployment-variables) need to explicitly provide the `KUBE_NAMESPACE` [deployment variable](#deployment-variables)
that will be used by your deployment jobs, otherwise a namespace will be created for you. that will be used by your deployment jobs, otherwise a namespace will be created for you.
NOTE: **Note:** #### Important notes
If you [install applications](#installing-applications) on your cluster, GitLab will create
the resources required to run these even if you have chosen to manage your own cluster. Note the following with GitLab and clusters:
- If you [install applications](#installing-applications) on your cluster, GitLab will
create the resources required to run these even if you have chosen to manage your own
cluster.
- Be aware that manually managing resources that have been created by GitLab, like
namespaces and service accounts, can cause unexpected errors. If this occurs, try
[clearing the cluster cache](#clearing-the-cluster-cache).
#### Clearing the cluster cache #### Clearing the cluster cache
......
...@@ -92,17 +92,17 @@ project. ...@@ -92,17 +92,17 @@ project.
## Editing a release ## Editing a release
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.5. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.6.
To edit the details of a release, navigate to **Project overview > Releases** and click To edit the details of a release, navigate to **Project overview > Releases** and click
the edit button (pencil icon) in the top-right corner of the release you want to modify. the edit button (pencil icon) in the top-right corner of the release you want to modify.
![A release with an edit button](img/release_edit_button_v12_5.png) ![A release with an edit button](img/release_edit_button_v12_6.png)
This will bring you to the **Edit Release** page, from which you can This will bring you to the **Edit Release** page, from which you can
change some of the release's details. change some of the release's details.
![Edit release page](img/edit_release_page_v12_5.png) ![Edit release page](img/edit_release_page_v12_6.png)
Currently, it is only possible to edit the release title and notes. Currently, it is only possible to edit the release title and notes.
To change other release information, such as its tag, associated To change other release information, such as its tag, associated
......
...@@ -40,6 +40,8 @@ const moduleNameMapper = { ...@@ -40,6 +40,8 @@ const moduleNameMapper = {
'^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants', '^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
}; };
const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
if (IS_EE) { if (IS_EE) {
const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1'; const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1';
Object.assign(moduleNameMapper, { Object.assign(moduleNameMapper, {
...@@ -47,6 +49,8 @@ if (IS_EE) { ...@@ -47,6 +49,8 @@ if (IS_EE) {
'^ee_component(/.*)$': rootDirEE, '^ee_component(/.*)$': rootDirEE,
'^ee_else_ce(/.*)$': rootDirEE, '^ee_else_ce(/.*)$': rootDirEE,
}); });
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
} }
// eslint-disable-next-line import/no-commonjs // eslint-disable-next-line import/no-commonjs
...@@ -54,7 +58,7 @@ module.exports = { ...@@ -54,7 +58,7 @@ module.exports = {
testMatch, testMatch,
moduleFileExtensions: ['js', 'json', 'vue'], moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper, moduleNameMapper,
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'], collectCoverageFrom,
coverageDirectory: '<rootDir>/coverage-frontend/', coverageDirectory: '<rootDir>/coverage-frontend/',
coverageReporters: ['json', 'lcov', 'text-summary', 'clover'], coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
cacheDirectory: '<rootDir>/tmp/cache/jest', cacheDirectory: '<rootDir>/tmp/cache/jest',
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
delegate :dig, to: :@seed_attributes delegate :dig, to: :@seed_attributes
# When the `ci_dag_limit_needs` is enabled it uses the lower limit # When the `ci_dag_limit_needs` is enabled it uses the lower limit
LOW_NEEDS_LIMIT = 5 LOW_NEEDS_LIMIT = 10
HARD_NEEDS_LIMIT = 50 HARD_NEEDS_LIMIT = 50
def initialize(pipeline, attributes, previous_stages) def initialize(pipeline, attributes, previous_stages)
......
...@@ -15,6 +15,7 @@ module Gitlab ...@@ -15,6 +15,7 @@ module Gitlab
:first_seen, :first_seen,
:frequency, :frequency,
:gitlab_project, :gitlab_project,
:gitlab_issue,
:id, :id,
:last_release_last_commit, :last_release_last_commit,
:last_release_short_version, :last_release_short_version,
......
...@@ -29,6 +29,7 @@ tree: ...@@ -29,6 +29,7 @@ tree:
- :priorities - :priorities
- :issue_assignees - :issue_assignees
- :zoom_meetings - :zoom_meetings
- :sentry_issue
- snippets: - snippets:
- :award_emoji - :award_emoji
- notes: - notes:
......
...@@ -94,7 +94,7 @@ module Gitlab ...@@ -94,7 +94,7 @@ module Gitlab
relation_index: relation_index, relation_index: relation_index,
exception_class: exception.class.to_s, exception_class: exception.class.to_s,
exception_message: exception.message.truncate(255), exception_message: exception.message.truncate(255),
correlation_id_value: Labkit::Correlation::CorrelationId.current_id correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
) )
end end
......
...@@ -233,6 +233,15 @@ module Sentry ...@@ -233,6 +233,15 @@ module Sentry
stack_trace_entry.dig('stacktrace', 'frames') stack_trace_entry.dig('stacktrace', 'frames')
end end
def parse_gitlab_issue(plugin_issues)
return unless plugin_issues
gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' }
return unless gitlab_plugin
gitlab_plugin.dig('issue', 'url')
end
def map_to_detailed_error(issue) def map_to_detailed_error(issue)
Gitlab::ErrorTracking::DetailedError.new( Gitlab::ErrorTracking::DetailedError.new(
id: issue.fetch('id'), id: issue.fetch('id'),
...@@ -252,6 +261,7 @@ module Sentry ...@@ -252,6 +261,7 @@ module Sentry
project_id: issue.dig('project', 'id'), project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'), project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug'), project_slug: issue.dig('project', 'slug'),
gitlab_issue: parse_gitlab_issue(issue.fetch('pluginIssues', nil)),
first_release_last_commit: issue.dig('firstRelease', 'lastCommit'), first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'), last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
first_release_short_version: issue.dig('firstRelease', 'shortVersion'), first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
......
...@@ -8360,6 +8360,9 @@ msgstr "" ...@@ -8360,6 +8360,9 @@ msgstr ""
msgid "GitLab Import" msgid "GitLab Import"
msgstr "" msgstr ""
msgid "GitLab Issue"
msgstr ""
msgid "GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com)." msgid "GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com)."
msgstr "" msgstr ""
......
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'gitlab'
require 'optparse'
#
# Configure credentials to be used with gitlab gem
#
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
config.private_token = ENV['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN']
end
options = {}
OptionParser.new do |opts|
opts.on("-s", "--scope=SCOPE", "Find job with matching scope") do |scope|
options[:scope] = scope
end
end.parse!
class PipelineJobFinder
def initialize(project_id, pipeline_id, job_name, options)
@project_id = project_id
@pipeline_id = pipeline_id
@job_name = job_name
@options = options
end
def execute
Gitlab.pipeline_jobs(@project_id, @pipeline_id, @options).auto_paginate do |job|
break job if job.name == @job_name
end
end
end
project_id, pipeline_id, job_name = ARGV
job = PipelineJobFinder.new(project_id, pipeline_id, job_name, options).execute
return if job.nil?
puts job.id
# Sends Slack notification MSG to CI_SLACK_WEBHOOK_URL (which needs to be set).
# ICON_EMOJI needs to be set to an icon emoji name (without the `:` around it).
function notify_slack() {
CHANNEL=$1
MSG=$2
ICON_EMOJI=$3
if [ -z "$CHANNEL" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ] || [ -z "$MSG" ] || [ -z "$ICON_EMOJI" ]; then
echo "Missing argument(s) - Use: $0 channel message icon_emoji"
echo "and set CI_SLACK_WEBHOOK_URL environment variable."
else
curl -X POST --data-urlencode 'payload={"channel": "#'"${CHANNEL}"'", "username": "GitLab QA Bot", "text": "'"${MSG}"'", "icon_emoji": "'":${ICON_EMOJI}:"'"}' "${CI_SLACK_WEBHOOK_URL}"
fi
}
function notify_on_job_failure() {
JOB_NAME=$1
CHANNEL=$2
MSG=$3
ICON_EMOJI=$4
local job_id
job_id=$(scripts/get-job-id "$CI_PROJECT_ID" "$CI_PIPELINE_ID" "$JOB_NAME" -s failed)
if [ -n "${job_id}" ]; then
notify_slack "${CHANNEL}" "${MSG}" "${ICON_EMOJI}"
fi
}
#!/bin/bash
# Sends Slack notification MSG to CI_SLACK_WEBHOOK_URL (which needs to be set).
# ICON_EMOJI needs to be set to an icon emoji name (without the `:` around it).
CHANNEL=$1
MSG=$2
ICON_EMOJI=$3
if [ -z "$CHANNEL" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ] || [ -z "$MSG" ] || [ -z "$ICON_EMOJI" ]; then
echo "Missing argument(s) - Use: $0 channel message icon_emoji"
echo "and set CI_SLACK_WEBHOOK_URL environment variable."
else
curl -X POST --data-urlencode 'payload={"channel": "#'"$CHANNEL"'", "username": "GitLab QA Bot", "text": "'"$MSG"'", "icon_emoji": "'":$ICON_EMOJI:"'"}' "$CI_SLACK_WEBHOOK_URL"
fi
...@@ -8,6 +8,8 @@ global: ...@@ -8,6 +8,8 @@ global:
configureCertmanager: false configureCertmanager: false
tls: tls:
secretName: tls-cert secretName: tls-cert
initialRootPassword:
secret: shared-gitlab-initial-root-password
certmanager: certmanager:
install: false install: false
gitlab: gitlab:
...@@ -26,8 +28,6 @@ gitlab: ...@@ -26,8 +28,6 @@ gitlab:
mailroom: mailroom:
enabled: false enabled: false
migrations: migrations:
initialRootPassword:
secret: shared-gitlab-initial-root-password
resources: resources:
requests: requests:
cpu: 350m cpu: 350m
......
...@@ -23,6 +23,7 @@ FactoryBot.define do ...@@ -23,6 +23,7 @@ FactoryBot.define do
[Time.now.to_i, 10] [Time.now.to_i, 10]
] ]
end end
gitlab_issue { 'http://gitlab.example.com/issues/1' }
first_release_last_commit { '68c914da9' } first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' } last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' } first_release_short_version { 'abc123' }
......
# frozen_string_literal: true
FactoryBot.define do
factory :sentry_issue, class: SentryIssue do
issue
sentry_issue_identifier { 1234567891 }
end
end
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"short_id", "short_id",
"status", "status",
"frequency", "frequency",
"gitlab_issue",
"first_release_last_commit", "first_release_last_commit",
"last_release_last_commit", "last_release_last_commit",
"first_release_short_version", "first_release_short_version",
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
"short_id": { "type": "string"}, "short_id": { "type": "string"},
"status": { "type": "string"}, "status": { "type": "string"},
"frequency": { "type": "array"}, "frequency": { "type": "array"},
"gitlab_issue": { "type": ["string", "null"] },
"first_release_last_commit": { "type": ["string", "null"] }, "first_release_last_commit": { "type": ["string", "null"] },
"last_release_last_commit": { "type": ["string", "null"] }, "last_release_last_commit": { "type": ["string", "null"] },
"first_release_short_version": { "type": ["string", "null"] }, "first_release_short_version": { "type": ["string", "null"] },
......
...@@ -366,7 +366,12 @@ ...@@ -366,7 +366,12 @@
"type": "ProjectLabel" "type": "ProjectLabel"
} }
} }
] ],
"sentry_issue": {
"id": 1,
"issue_id": 40,
"sentry_issue_identifier": 1234567891
}
}, },
{ {
"id": 39, "id": 39,
......
...@@ -138,5 +138,48 @@ describe('ErrorDetails', () => { ...@@ -138,5 +138,48 @@ describe('ErrorDetails', () => {
submitSpy.mockRestore(); submitSpy.mockRestore();
}); });
}); });
describe('GitLab issue link', () => {
const gitlabIssue = 'https://gitlab.example.com/issues/1';
const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`);
const findCreateIssueButton = () => wrapper.find('[data-qa-selector="create_issue_button"]');
describe('is present', () => {
beforeEach(() => {
store.state.details.loading = false;
store.state.details.error = {
id: 1,
gitlab_issue: gitlabIssue,
};
mountComponent();
});
it('should display the issue link', () => {
expect(findGitLabLink().exists()).toBe(true);
});
it('should not display a create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(false);
});
});
describe('is not present', () => {
beforeEach(() => {
store.state.details.loading = false;
store.state.details.error = {
id: 1,
gitlab_issue: null,
};
mountComponent();
});
it('should not display an issue link', () => {
expect(findGitLabLink().exists()).toBe(false);
});
it('should display the create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(true);
});
});
});
}); });
}); });
...@@ -852,7 +852,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -852,7 +852,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it "returns an error" do it "returns an error" do
expect(subject.errors).to contain_exactly( expect(subject.errors).to contain_exactly(
"rspec: one job can only need 5 others, but you have listed 6. See needs keyword documentation for more details") "rspec: one job can only need 10 others, but you have listed 11. See needs keyword documentation for more details")
end end
end end
......
...@@ -8,6 +8,7 @@ issues: ...@@ -8,6 +8,7 @@ issues:
- milestone - milestone
- notes - notes
- resource_label_events - resource_label_events
- sentry_issue
- label_links - label_links
- labels - labels
- last_edited_by - last_edited_by
...@@ -548,4 +549,6 @@ versions: &version ...@@ -548,4 +549,6 @@ versions: &version
- actions - actions
zoom_meetings: zoom_meetings:
- issue - issue
sentry_issue:
- issue
design_versions: *version design_versions: *version
...@@ -234,6 +234,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -234,6 +234,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(meetings.first.url).to eq('https://zoom.us/j/123456789') expect(meetings.first.url).to eq('https://zoom.us/j/123456789')
end end
it 'restores sentry issues' do
sentry_issue = @project.issues.first.sentry_issue
expect(sentry_issue.sentry_issue_identifier).to eq(1234567891)
end
context 'Merge requests' do context 'Merge requests' do
it 'always has the new project as a target' do it 'always has the new project as a target' do
expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
...@@ -643,7 +649,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -643,7 +649,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
before do before do
setup_import_export_config('with_invalid_records') setup_import_export_config('with_invalid_records')
Labkit::Correlation::CorrelationId.use_id(correlation_id) { subject } # Import is running from the rake task, `correlation_id` is not assigned
expect(Labkit::Correlation::CorrelationId).to receive(:new_id).and_return(correlation_id)
subject
end end
context 'when failures occur because a relation fails to be processed' do context 'when failures occur because a relation fails to be processed' do
......
...@@ -689,6 +689,10 @@ ErrorTracking::ProjectErrorTrackingSetting: ...@@ -689,6 +689,10 @@ ErrorTracking::ProjectErrorTrackingSetting:
- project_id - project_id
- project_name - project_name
- organization_name - organization_name
SentryIssue:
- id
- issue_id
- sentry_issue_identifier
Suggestion: Suggestion:
- id - id
- note_id - note_id
......
...@@ -12,6 +12,7 @@ describe Issue do ...@@ -12,6 +12,7 @@ describe Issue do
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') } it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
it { is_expected.to belong_to(:closed_by).class_name('User') } it { is_expected.to belong_to(:closed_by).class_name('User') }
it { is_expected.to have_many(:assignees) } it { is_expected.to have_many(:assignees) }
it { is_expected.to have_one(:sentry_issue) }
end end
describe 'modules' do describe 'modules' do
......
# frozen_string_literal: true
require 'spec_helper'
describe SentryIssue do
describe 'associations' do
it { is_expected.to belong_to(:issue) }
end
describe 'validations' do
let!(:sentry_issue) { create(:sentry_issue) }
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_uniqueness_of(:issue) }
it { is_expected.to validate_presence_of(:sentry_issue_identifier) }
end
end
...@@ -41,4 +41,30 @@ RSpec.describe Timelog do ...@@ -41,4 +41,30 @@ RSpec.describe Timelog do
expect(subject).to be_valid expect(subject).to be_valid
end end
end end
describe 'scopes' do
describe 'for_issues_in_group' do
it 'return timelogs created for group issues' do
group = create(:group)
subgroup = create(:group, parent: group)
create(:timelog, issue: create(:issue, project: create(:project)))
timelog1 = create(:timelog, issue: create(:issue, project: create(:project, group: group)))
timelog2 = create(:timelog, issue: create(:issue, project: create(:project, group: subgroup)))
expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
end
end
describe 'between_dates' do
it 'returns collection of timelogs within given dates' do
create(:timelog, spent_at: 65.days.ago)
timelog1 = create(:timelog, spent_at: 15.days.ago)
timelog2 = create(:timelog, spent_at: 5.days.ago)
timelogs = described_class.between_dates(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
end
end end
...@@ -6,11 +6,17 @@ module LiveDebugger ...@@ -6,11 +6,17 @@ module LiveDebugger
def live_debug def live_debug
puts puts
puts "Current example is paused for live debugging." puts "Current example is paused for live debugging."
puts "Opening #{current_url} in your default browser..."
if ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
puts "Switch to the Chrome window that was automatically opened to run the test in order to view current page"
else
puts "Opening #{current_url} in your default browser..."
end
puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user
puts "Press any key to resume the execution of the example!!" puts "Press any key to resume the execution of the example!!"
`open #{current_url}` `open #{current_url}` if ENV['CHROME_HEADLESS'] !~ /^(false|no|0)$/i
loop until $stdin.getch loop until $stdin.getch
......
...@@ -64,6 +64,12 @@ RSpec::Matchers.define :have_graphql_type do |expected| ...@@ -64,6 +64,12 @@ RSpec::Matchers.define :have_graphql_type do |expected|
end end
end end
RSpec::Matchers.define :have_non_null_graphql_type do |expected|
match do |field|
expect(field.type).to eq(!expected.to_graphql)
end
end
RSpec::Matchers.define :have_graphql_resolver do |expected| RSpec::Matchers.define :have_graphql_resolver do |expected|
match do |field| match do |field|
case expected case expected
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment