From 6fa3630aad333511c687b385c2333e98e09595b4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 17 Nov 2020 09:09:23 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitpod.yml | 4 +- .../issue_show/components/header_actions.vue | 57 +++++++- app/assets/javascripts/issue_show/issue.js | 1 + .../queries/promote_to_epic.mutation.graphql | 8 ++ .../javascripts/pages/groups/boards/index.js | 8 +- .../pages/groups/milestones/edit/index.js | 2 +- .../pages/groups/milestones/new/index.js | 2 +- .../javascripts/pages/groups/new/index.js | 16 +-- .../pages/groups/packages/index/index.js | 8 +- .../vue_shared/components/markdown/field.vue | 4 +- app/assets/stylesheets/themes/_dark.scss | 18 +++ .../projects/pipelines_controller.rb | 2 +- ...ontainer_repository_cleanup_status_enum.rb | 13 ++ .../types/container_repository_type.rb | 1 + .../projects/cycle_analytics/show.html.haml | 2 +- app/views/projects/pipelines/new.html.haml | 2 +- ...o-container-repository-details-graphql.yml | 5 + ...-fa-chevron-duown-in-project-level-vsa.yml | 5 + ...place-fa-exclamation-triangle-markdown.yml | 5 + .../new-pipeline-form-default-true.yml | 5 + .../development/new_pipeline_form.yml | 2 +- config/webpack.config.js | 2 + .../graphql/reference/gitlab_schema.graphql | 35 +++++ doc/api/graphql/reference/gitlab_schema.json | 63 +++++++++ doc/api/graphql/reference/index.md | 13 ++ doc/development/api_graphql_styleguide.md | 2 +- .../security_dashboard/index.md | 12 ++ doc/user/project/issues/index.md | 3 +- doc/user/project/issues/managing_issues.md | 29 +++- locale/gitlab.pot | 18 ++- .../graphql/container_repositories.json | 12 ++ .../schemas/graphql/container_repository.json | 6 +- .../graphql/container_repository_details.json | 33 +---- .../components/header_actions_spec.js | 131 ++++++++++++++++-- ...ner_repository_cleanup_status_enum_spec.rb | 13 ++ .../container_repository_details_type_spec.rb | 2 +- .../types/container_repository_type_spec.rb | 10 +- .../container_repository_details_spec.rb | 2 +- .../project/container_repositories_spec.rb | 4 + 39 files changed, 471 insertions(+), 89 deletions(-) create mode 100644 app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql create mode 100644 app/graphql/types/container_repository_cleanup_status_enum.rb create mode 100644 changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml create mode 100644 changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml create mode 100644 changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml create mode 100644 changelogs/unreleased/new-pipeline-form-default-true.yml create mode 100644 spec/fixtures/api/schemas/graphql/container_repositories.json create mode 100644 spec/graphql/types/container_repository_cleanup_status_enum_spec.rb diff --git a/.gitpod.yml b/.gitpod.yml index 6a771e77769..dc1dbc1472d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -15,9 +15,6 @@ tasks: cd /workspace/gitlab-development-kit [[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config - # make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace - echo "webpack:" >> gdk.yml - echo " static: true" >> gdk.yml # reconfigure GDK echo "$(date) – Reconfiguring GDK" | tee -a /workspace/startup.log gdk reconfigure @@ -43,6 +40,7 @@ tasks: fi # start GDK echo "$(date) – Starting GDK" | tee -a /workspace/startup.log + export DEV_SERVER_PUBLIC_ADDR=$(gp url 3808) export RAILS_HOSTS=$(gp url 3000 | sed -e 's+^http[s]*://++') gdk start # Run DB migrations diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue index 165fa745485..ae0aca0fd27 100644 --- a/app/assets/javascripts/issue_show/components/header_actions.vue +++ b/app/assets/javascripts/issue_show/components/header_actions.vue @@ -1,11 +1,13 @@ @@ -152,6 +196,9 @@ export default { {{ newIssueTypeText }} + + {{ __('Promote to epic') }} + {{ __('Report abuse') }} @@ -190,6 +237,14 @@ export default { {{ newIssueTypeText }} + + {{ __('Promote to epic') }} + {{ __('Report abuse') }} diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index 3d0d8d882be..8260460828b 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -45,6 +45,7 @@ export function initIssueHeaderActions(store) { store, provide: { canCreateIssue: parseBoolean(el.dataset.canCreateIssue), + canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic), canReopenIssue: parseBoolean(el.dataset.canReopenIssue), canReportSpam: parseBoolean(el.dataset.canReportSpam), canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue), diff --git a/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql b/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql new file mode 100644 index 00000000000..12d05af0f5e --- /dev/null +++ b/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql @@ -0,0 +1,8 @@ +mutation promoteToEpic($input: PromoteToEpicInput!) { + promoteToEpic(input: $input) { + epic { + webPath + } + errors + } +} diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js index 79c3be771d0..922f39627c9 100644 --- a/app/assets/javascripts/pages/groups/boards/index.js +++ b/app/assets/javascripts/pages/groups/boards/index.js @@ -2,8 +2,6 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initBoards from '~/boards'; -document.addEventListener('DOMContentLoaded', () => { - new UsersSelect(); // eslint-disable-line no-new - new ShortcutsNavigation(); // eslint-disable-line no-new - initBoards(); -}); +new UsersSelect(); // eslint-disable-line no-new +new ShortcutsNavigation(); // eslint-disable-line no-new +initBoards(); diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js index ddd10fe5062..af0264c7992 100644 --- a/app/assets/javascripts/pages/groups/milestones/edit/index.js +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -document.addEventListener('DOMContentLoaded', () => initForm(false)); +initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js index ddd10fe5062..af0264c7992 100644 --- a/app/assets/javascripts/pages/groups/milestones/new/index.js +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -1,3 +1,3 @@ import initForm from '../../../../shared/milestones/form'; -document.addEventListener('DOMContentLoaded', () => initForm(false)); +initForm(false); diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 640e64b5d3e..7021473b380 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -4,13 +4,11 @@ import Group from '~/group'; import GroupPathValidator from './group_path_validator'; import initFilePickers from '~/file_pickers'; -document.addEventListener('DOMContentLoaded', () => { - const parentId = $('#group_parent_id'); - if (!parentId.val()) { - new GroupPathValidator(); // eslint-disable-line no-new - } - BindInOut.initAll(); - initFilePickers(); +const parentId = $('#group_parent_id'); +if (!parentId.val()) { + new GroupPathValidator(); // eslint-disable-line no-new +} +BindInOut.initAll(); +initFilePickers(); - return new Group(); -}); +new Group(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js index 4836900aa28..1c4a10fd653 100644 --- a/app/assets/javascripts/pages/groups/packages/index/index.js +++ b/app/assets/javascripts/pages/groups/packages/index/index.js @@ -1,7 +1,5 @@ import initPackageList from '~/packages/list/packages_list_app_bundle'; -document.addEventListener('DOMContentLoaded', () => { - if (document.getElementById('js-vue-packages-list')) { - initPackageList(); - } -}); +if (document.getElementById('js-vue-packages-list')) { + initPackageList(); +} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 1808e4d6ad4..893c0a0a5b9 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -141,10 +141,9 @@ export default { addMultipleToDiscussionWarning() { return sprintf( __( - '%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification.', + 'You are about to add %{usersTag} people to the discussion. They will all receive a notification.', ), { - icon: '', usersTag: `${this.referencedUsers.length}`, }, false, @@ -293,6 +292,7 @@ export default { diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index 66cc4452858..6ab02bd5e27 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -201,6 +201,15 @@ $line-removed-dark: $red-200; // Misc component overrides that should live elsewhere .gl-label { filter: brightness(0.9) contrast(1.1); + + // This applies to the gl-label markups + // rendered and cached in the backend (labels_helper.rb) + &.gl-label-scoped { + .gl-label-text-scoped, + .gl-label-close { + color: $gray-900; + } + } } // white-ish text for light labels @@ -210,6 +219,15 @@ $line-removed-dark: $red-200; color: $gray-900; } +// This applies to "gl-labels" from "gitlab-ui" +.gl-label.gl-label-scoped.gl-label-text-dark, +.gl-label.gl-label-scoped.gl-label-text-light { + .gl-label-text-scoped, + .gl-label-close { + color: $gray-900; + } +} + // duplicated class as the original .atwho-view style is added later .atwho-view.atwho-view { background-color: $white; diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index c576b700ac1..f71a92ee874 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -14,7 +14,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action do push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true) push_frontend_feature_flag(:pipelines_security_report_summary, project) - push_frontend_feature_flag(:new_pipeline_form, project) + push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false) push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development) diff --git a/app/graphql/types/container_repository_cleanup_status_enum.rb b/app/graphql/types/container_repository_cleanup_status_enum.rb new file mode 100644 index 00000000000..6e654e65360 --- /dev/null +++ b/app/graphql/types/container_repository_cleanup_status_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryCleanupStatusEnum < BaseEnum + graphql_name 'ContainerRepositoryCleanupStatus' + description 'Status of the tags cleanup of a container repository' + + value 'UNSCHEDULED', value: 'cleanup_unscheduled', description: 'The tags cleanup is not scheduled. This is the default state.' + value 'SCHEDULED', value: 'cleanup_scheduled', description: 'The tags cleanup is scheduled and is going to be executed shortly.' + value 'UNFINISHED', value: 'cleanup_unfinished', description: 'The tags cleanup has been partially executed. There are still remaining tags to delete.' + value 'ONGOING', value: 'cleanup_ongoing', description: 'The tags cleanup is ongoing.' + end +end diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb index 2c8382ea058..45d19fdbc50 100644 --- a/app/graphql/types/container_repository_type.rb +++ b/app/graphql/types/container_repository_type.rb @@ -15,6 +15,7 @@ module Types field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.' field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.' field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.' + field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'The tags cleanup status for the container repository.' field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.' field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.' field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.' diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index d99579c25c0..b98ab9757fa 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -22,7 +22,7 @@ .dropdown.inline.js-ca-dropdown %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} - %i.fa.fa-chevron-down + = sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3") %ul.dropdown-menu.dropdown-menu-right %li %a{ "href" => "#", "data-value" => "7" } diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index bddd542325b..bc8e6a6d9cc 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -6,7 +6,7 @@ = s_('Pipeline|Run Pipeline') %hr -- if Feature.enabled?(:new_pipeline_form, @project) +- if Feature.enabled?(:new_pipeline_form, @project, default_enabled: true) #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project), diff --git a/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml b/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml new file mode 100644 index 00000000000..ebe84b20539 --- /dev/null +++ b/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add cleanup status field to graphQL ContainerRepositoryType +merge_request: 47544 +author: +type: added diff --git a/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml b/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml new file mode 100644 index 00000000000..9055a181995 --- /dev/null +++ b/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-chevron-down in project level VSA +merge_request: 47885 +author: +type: changed diff --git a/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml b/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml new file mode 100644 index 00000000000..f1ba024beb7 --- /dev/null +++ b/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-exclamation-triangle in markdown field MERGE_REQUEST_ID +merge_request: 47786 +author: +type: changed diff --git a/changelogs/unreleased/new-pipeline-form-default-true.yml b/changelogs/unreleased/new-pipeline-form-default-true.yml new file mode 100644 index 00000000000..dd12ef458ec --- /dev/null +++ b/changelogs/unreleased/new-pipeline-form-default-true.yml @@ -0,0 +1,5 @@ +--- +title: Default enable new_pipeline_form +merge_request: 46915 +author: +type: added diff --git a/config/feature_flags/development/new_pipeline_form.yml b/config/feature_flags/development/new_pipeline_form.yml index 4c0ba5af350..3a4b30f4bd9 100644 --- a/config/feature_flags/development/new_pipeline_form.yml +++ b/config/feature_flags/development/new_pipeline_form.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229632 milestone: '13.2' type: development group: group::continuous integration -default_enabled: false +default_enabled: true diff --git a/config/webpack.config.js b/config/webpack.config.js index fe256e17e73..c72583e4b24 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -18,6 +18,7 @@ const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true'; const IS_EE = require('./helpers/is_ee_env'); const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; +const DEV_SERVER_PUBLIC_ADDR = process.env.DEV_SERVER_PUBLIC_ADDR; const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false'; const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false'; const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false'; @@ -554,6 +555,7 @@ module.exports = { devServer: { host: DEV_SERVER_HOST, port: DEV_SERVER_PORT, + public: DEV_SERVER_PUBLIC_ADDR, https: DEV_SERVER_HTTPS, headers: { 'Access-Control-Allow-Origin': '*', diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index c4232de2b98..7335b35f432 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3313,6 +3313,11 @@ type ContainerRepository { """ createdAt: Time! + """ + The tags cleanup status for the container repository. + """ + expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus + """ Timestamp when the cleanup done by the expiration policy was started on the container repository. """ @@ -3354,6 +3359,31 @@ type ContainerRepository { updatedAt: Time! } +""" +Status of the tags cleanup of a container repository +""" +enum ContainerRepositoryCleanupStatus { + """ + The tags cleanup is ongoing. + """ + ONGOING + + """ + The tags cleanup is scheduled and is going to be executed shortly. + """ + SCHEDULED + + """ + The tags cleanup has been partially executed. There are still remaining tags to delete. + """ + UNFINISHED + + """ + The tags cleanup is not scheduled. This is the default state. + """ + UNSCHEDULED +} + """ The connection type for ContainerRepository. """ @@ -3388,6 +3418,11 @@ type ContainerRepositoryDetails { """ createdAt: Time! + """ + The tags cleanup status for the container repository. + """ + expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus + """ Timestamp when the cleanup done by the expiration policy was started on the container repository. """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 305d7dbf08f..4539c6806e1 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -8947,6 +8947,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "expirationPolicyCleanupStatus", + "description": "The tags cleanup status for the container repository.", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "ContainerRepositoryCleanupStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "expirationPolicyStartedAt", "description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.", @@ -9091,6 +9105,41 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "ContainerRepositoryCleanupStatus", + "description": "Status of the tags cleanup of a container repository", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "UNSCHEDULED", + "description": "The tags cleanup is not scheduled. This is the default state.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEDULED", + "description": "The tags cleanup is scheduled and is going to be executed shortly.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNFINISHED", + "description": "The tags cleanup has been partially executed. There are still remaining tags to delete.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ONGOING", + "description": "The tags cleanup is ongoing.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "ContainerRepositoryConnection", @@ -9199,6 +9248,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "expirationPolicyCleanupStatus", + "description": "The tags cleanup status for the container repository.", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "ContainerRepositoryCleanupStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "expirationPolicyStartedAt", "description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 64334895135..f0e0d56d47d 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -535,6 +535,7 @@ A container repository. | ----- | ---- | ----------- | | `canDelete` | Boolean! | Can the current user delete the container repository. | | `createdAt` | Time! | Timestamp when the container repository was created. | +| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. | | `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. | | `id` | ID! | ID of the container repository. | | `location` | String! | URL of the container repository. | @@ -552,6 +553,7 @@ Details of a container repository. | ----- | ---- | ----------- | | `canDelete` | Boolean! | Can the current user delete the container repository. | | `createdAt` | Time! | Timestamp when the container repository was created. | +| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. | | `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. | | `id` | ID! | ID of the container repository. | | `location` | String! | URL of the container repository. | @@ -3812,6 +3814,17 @@ Mode of a commit action. | `SEVEN_DAYS` | 7 days until tags are automatically removed | | `THIRTY_DAYS` | 30 days until tags are automatically removed | +### ContainerRepositoryCleanupStatus + +Status of the tags cleanup of a container repository. + +| Value | Description | +| ----- | ----------- | +| `ONGOING` | The tags cleanup is ongoing. | +| `SCHEDULED` | The tags cleanup is scheduled and is going to be executed shortly. | +| `UNFINISHED` | The tags cleanup has been partially executed. There are still remaining tags to delete. | +| `UNSCHEDULED` | The tags cleanup is not scheduled. This is the default state. | + ### ContainerRepositoryStatus Status of a container repository. diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 231b3bcfd88..0e81a332d6c 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -397,7 +397,7 @@ field :foo, GraphQL::STRING_TYPE, 'if `my_feature_flag` feature flag is disabled' def foo - object.foo unless Feature.enabled?(:my_feature_flag, object) + object.foo if Feature.enabled?(:my_feature_flag, object) end ``` diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 9f402cea9dc..1b038ef76a0 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -63,6 +63,8 @@ job finishes but the DAST job fails, the security dashboard doesn't show SAST re the analyzer outputs an [exit code](../../../development/integrations/secure.md#exit-code). +You can filter the vulnerabilities list by selecting from the **Severity** and **Scanner** dropdowns. + ## Project Security Dashboard > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6. @@ -105,6 +107,11 @@ You can filter the vulnerabilities by one or more of the following: | Severity | Critical, High, Medium, Low, Info, Unknown | | Scanner | [Available Scanners](../index.md#security-scanning-tools) | +You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and +**Scanner** dropdowns. In the **Scanner** dropdown, select individual scanners or scanner groups to +toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and in GitLab 13.6 +and later, custom scanners. + You can also dismiss vulnerabilities in the table: 1. Select the checkbox for each vulnerability you want to dismiss. @@ -260,6 +267,11 @@ You can filter which vulnerabilities the vulnerability report displays by: | Scanner | [Available Scanners](../index.md#security-scanning-tools) | | Project | Projects configured in the Security Center settings | +You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and +**Scanner**, and **Project** dropdowns. In the **Scanner** dropdown, select individual scanners or +scanner groups to toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and +in GitLab 13.6 and later, custom scanners. + Clicking any vulnerability in the table takes you to its [Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability. To create an issue associated with the vulnerability, click the **Create Issue** button. diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 6be71f540ad..716377f2e45 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -95,12 +95,13 @@ While you can view and manage the full details of an issue on the [issue page](# you can also work with multiple issues at a time using the [Issues List](#issues-list), [Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**. -Key actions for Issues include: +Key actions for issues include: - [Creating issues](managing_issues.md#create-a-new-issue) - [Moving issues](managing_issues.md#moving-issues) - [Closing issues](managing_issues.md#closing-issues) - [Deleting issues](managing_issues.md#deleting-issues) +- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic) **(PREMIUM)** ### Issue page diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 8b5d2fcb2e8..62b388ec137 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -7,9 +7,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Managing issues [GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and -planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues), -[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that -you can do with issues. +planning work in GitLab. + +Key actions for issues include: + +- [Creating issues](#create-a-new-issue) +- [Moving issues](#moving-issues) +- [Closing issues](#closing-issues) +- [Deleting issues](#deleting-issues) +- [Promoting issues](#promote-an-issue-to-an-epic) **(PREMIUM)** ## Create a new issue @@ -280,6 +286,23 @@ editing it and clicking on the delete button. ![delete issue - button](img/delete_issue.png) +## Promote an issue to an epic **(PREMIUM)** + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8. +> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6. + +You can promote an issue to an epic in the immediate parent group. + +To promote an issue to an epic: + +1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button. +1. Select **Promote to epic**. + +Alternatively, you can use the `/promote` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics). + +Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic). + ## Add an issue to an iteration **(STARTER)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cb068850413..77a877ebde2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -528,9 +528,6 @@ msgstr "" msgid "%{host} sign-in from new location" msgstr "" -msgid "%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification." -msgstr "" - msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}." msgstr "" @@ -7707,6 +7704,9 @@ msgstr "" msgid "Could not restore the group" msgstr "" +msgid "Could not retrieve custom scanners for scanner filter. Please try again later." +msgstr "" + msgid "Could not revoke impersonation token %{token_name}." msgstr "" @@ -21810,6 +21810,9 @@ msgstr "" msgid "Promote issue to an epic" msgstr "" +msgid "Promote to epic" +msgstr "" + msgid "Promote to group label" msgstr "" @@ -25341,6 +25344,9 @@ msgstr "" msgid "Something went wrong while performing the action." msgstr "" +msgid "Something went wrong while promoting the issue to an epic. Please try again." +msgstr "" + msgid "Something went wrong while reopening a requirement." msgstr "" @@ -26956,6 +26962,9 @@ msgstr "" msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" +msgid "The issue was successfully promoted to an epic. Redirecting to epic..." +msgstr "" + msgid "The license for Deploy Board is required to use this feature." msgstr "" @@ -30764,6 +30773,9 @@ msgstr "" msgid "You already have pending todo for this alert" msgstr "" +msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification." +msgstr "" + msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application." msgstr "" diff --git a/spec/fixtures/api/schemas/graphql/container_repositories.json b/spec/fixtures/api/schemas/graphql/container_repositories.json new file mode 100644 index 00000000000..8e8982ff8c7 --- /dev/null +++ b/spec/fixtures/api/schemas/graphql/container_repositories.json @@ -0,0 +1,12 @@ +{ + "type": "array", + "items": { + "type": "object", + "required": ["node"], + "properties": { + "node": { + "$ref": "./container_repository.json" + } + } + } +} diff --git a/spec/fixtures/api/schemas/graphql/container_repository.json b/spec/fixtures/api/schemas/graphql/container_repository.json index 0737e71dd17..e252bedab82 100644 --- a/spec/fixtures/api/schemas/graphql/container_repository.json +++ b/spec/fixtures/api/schemas/graphql/container_repository.json @@ -1,6 +1,6 @@ { "type": "object", - "required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete"], + "required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus"], "properties": { "id": { "type": "string" @@ -31,6 +31,10 @@ }, "canDelete": { "type": "boolean" + }, + "expirationPolicyCleanupStatus": { + "type": "string", + "enum": ["UNSCHEDULED", "SCHEDULED", "UNFINISHED", "ONGOING"] } } } diff --git a/spec/fixtures/api/schemas/graphql/container_repository_details.json b/spec/fixtures/api/schemas/graphql/container_repository_details.json index b076711dcea..3db91796fc6 100644 --- a/spec/fixtures/api/schemas/graphql/container_repository_details.json +++ b/spec/fixtures/api/schemas/graphql/container_repository_details.json @@ -1,37 +1,8 @@ { "type": "object", - "required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "tags"], + "required": ["tags"], + "allOf": [{ "$ref": "./container_repository.json" }], "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "location": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "updatedAt": { - "type": "string" - }, - "expirationPolicyStartedAt": { - "type": ["string", "null"] - }, - "status": { - "type": ["string", "null"] - }, - "tagsCount": { - "type": "integer" - }, - "canDelete": { - "type": "boolean" - }, "tags": { "type": "object", "required": ["nodes"], diff --git a/spec/frontend/issue_show/components/header_actions_spec.js b/spec/frontend/issue_show/components/header_actions_spec.js index ec9f8ea1dc8..67b8665a889 100644 --- a/spec/frontend/issue_show/components/header_actions_spec.js +++ b/spec/frontend/issue_show/components/header_actions_spec.js @@ -1,14 +1,21 @@ import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; +import createFlash, { FLASH_TYPES } from '~/flash'; import { IssuableType } from '~/issuable_show/constants'; import HeaderActions from '~/issue_show/components/header_actions.vue'; import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants'; +import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql'; +import * as urlUtility from '~/lib/utils/url_utility'; import createStore from '~/notes/stores'; +jest.mock('~/flash'); + describe('HeaderActions component', () => { let dispatchEventSpy; + let mutateMock; let wrapper; + let visitUrlSpy; const localVue = createLocalVue(); localVue.use(Vuex); @@ -16,6 +23,7 @@ describe('HeaderActions component', () => { const defaultProps = { canCreateIssue: true, + canPromoteToEpic: true, canReopenIssue: true, canReportSpam: true, canUpdateIssue: true, @@ -29,7 +37,27 @@ describe('HeaderActions component', () => { submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam', }; - const mutate = jest.fn().mockResolvedValue({ data: { updateIssue: { errors: [] } } }); + const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } }; + + const promoteToEpicMutationResponse = { + data: { + promoteToEpic: { + errors: [], + epic: { + webPath: '/groups/gitlab-org/-/epics/1', + }, + }, + }, + }; + + const promoteToEpicMutationErrorResponse = { + data: { + promoteToEpic: { + errors: ['The issue has already been promoted to an epic.'], + epic: {}, + }, + }, + }; const findToggleIssueStateButton = () => wrapper.find(GlButton); @@ -50,7 +78,10 @@ describe('HeaderActions component', () => { props = {}, issueState = IssuableStatus.Open, blockedByIssues = [], + mutateResponse = {}, } = {}) => { + mutateMock = jest.fn().mockResolvedValue(mutateResponse); + store.getters.getNoteableData.state = issueState; store.getters.getNoteableData.blocked_by_issues = blockedByIssues; @@ -63,7 +94,7 @@ describe('HeaderActions component', () => { }, mocks: { $apollo: { - mutate, + mutate: mutateMock, }, }, }); @@ -73,6 +104,9 @@ describe('HeaderActions component', () => { if (dispatchEventSpy) { dispatchEventSpy.mockRestore(); } + if (visitUrlSpy) { + visitUrlSpy.mockRestore(); + } wrapper.destroy(); }); @@ -90,7 +124,11 @@ describe('HeaderActions component', () => { beforeEach(() => { dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); - wrapper = mountComponent({ props: { issueType }, issueState }); + wrapper = mountComponent({ + props: { issueType }, + issueState, + mutateResponse: updateIssueMutationResponse, + }); }); it(`has text "${buttonText}"`, () => { @@ -100,11 +138,11 @@ describe('HeaderActions component', () => { it('calls apollo mutation', () => { findToggleIssueStateButton().vm.$emit('click'); - expect(mutate).toHaveBeenCalledWith( + expect(mutateMock).toHaveBeenCalledWith( expect.objectContaining({ variables: { input: { - iid: defaultProps.iid.toString(), + iid: defaultProps.iid, projectPath: defaultProps.projectPath, stateEvent: newIssueState, }, @@ -129,15 +167,17 @@ describe('HeaderActions component', () => { ${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} `('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => { describe.each` - description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam - ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} - ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} - ${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} - ${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} - ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} - ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} - ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} - ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} + description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic + ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} + ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} + ${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} + ${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} + ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} + ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} + ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} + ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} + ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} + ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} `( '$description', ({ @@ -147,6 +187,7 @@ describe('HeaderActions component', () => { canCreateIssue, isIssueAuthor, canReportSpam, + canPromoteToEpic, }) => { beforeEach(() => { wrapper = mountComponent({ @@ -156,6 +197,7 @@ describe('HeaderActions component', () => { isIssueAuthor, issueType, canReportSpam, + canPromoteToEpic, }, }); }); @@ -172,6 +214,65 @@ describe('HeaderActions component', () => { }); }); + describe('when "Promote to epic" button is clicked', () => { + describe('when response is successful', () => { + beforeEach(() => { + visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); + + wrapper = mountComponent({ + mutateResponse: promoteToEpicMutationResponse, + }); + + wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + }); + + it('invokes GraphQL mutation when clicked', () => { + expect(mutateMock).toHaveBeenCalledWith( + expect.objectContaining({ + mutation: promoteToEpicMutation, + variables: { + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + }, + }, + }), + ); + }); + + it('shows a success message and tells the user they are being redirected', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: 'The issue was successfully promoted to an epic. Redirecting to epic...', + type: FLASH_TYPES.SUCCESS, + }); + }); + + it('redirects to newly created epic path', () => { + expect(visitUrlSpy).toHaveBeenCalledWith( + promoteToEpicMutationResponse.data.promoteToEpic.epic.webPath, + ); + }); + }); + + describe('when response contains errors', () => { + beforeEach(() => { + visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); + + wrapper = mountComponent({ + mutateResponse: promoteToEpicMutationErrorResponse, + }); + + wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + }); + + it('shows an error message', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: promoteToEpicMutationErrorResponse.data.promoteToEpic.errors.join('; '), + }); + }); + }); + }); + describe('modal', () => { const blockedByIssues = [ { iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' }, @@ -197,7 +298,7 @@ describe('HeaderActions component', () => { it('calls apollo mutation when primary button is clicked', () => { findModal().vm.$emit('primary'); - expect(mutate).toHaveBeenCalledWith( + expect(mutateMock).toHaveBeenCalledWith( expect.objectContaining({ variables: { input: { diff --git a/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb b/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb new file mode 100644 index 00000000000..36cfc789ee9 --- /dev/null +++ b/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ContainerRepositoryCleanupStatus'] do + it 'exposes all statuses' do + expected_keys = ContainerRepository.expiration_policy_cleanup_statuses + .keys + .map { |k| k.gsub('cleanup_', '') } + .map(&:upcase) + expect(described_class.values.keys).to contain_exactly(*expected_keys) + end +end diff --git a/spec/graphql/types/container_repository_details_type_spec.rb b/spec/graphql/types/container_repository_details_type_spec.rb index 13563dbb5aa..b5ff460fcf7 100644 --- a/spec/graphql/types/container_repository_details_type_spec.rb +++ b/spec/graphql/types/container_repository_details_type_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do - fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete tags] + fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status tags] it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') } diff --git a/spec/graphql/types/container_repository_type_spec.rb b/spec/graphql/types/container_repository_type_spec.rb index ec5bb14a483..3d3445ba5c3 100644 --- a/spec/graphql/types/container_repository_type_spec.rb +++ b/spec/graphql/types/container_repository_type_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['ContainerRepository'] do - fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete] + fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status] it { expect(described_class.graphql_name).to eq('ContainerRepository') } @@ -20,4 +20,12 @@ RSpec.describe GitlabSchema.types['ContainerRepository'] do is_expected.to have_graphql_type(Types::ContainerRepositoryStatusEnum) end end + + describe 'expiration_policy_cleanup_status field' do + subject { described_class.fields['expirationPolicyCleanupStatus'] } + + it 'returns cleanup status enum' do + is_expected.to have_graphql_type(Types::ContainerRepositoryCleanupStatusEnum) + end + end end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index a63adb8efc4..3c1c63c1670 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'container repository details' do subject end - it 'matches the expected schema' do + it 'matches the JSON schema' do expect(container_repository_details_response).to match_schema('graphql/container_repository_details') end end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index 428424802a2..7e32f54bf1d 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -47,6 +47,10 @@ RSpec.describe 'getting container repositories in a project' do before do subject end + + it 'matches the JSON schema' do + expect(container_repositories_response).to match_schema('graphql/container_repositories') + end end context 'with different permissions' do