summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md14
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js4
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue2
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js3
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue40
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue51
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue10
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue9
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue12
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue42
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue9
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue8
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss5
-rw-r--r--app/assets/stylesheets/framework/tables.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss7
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss8
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/tree_helper.rb20
-rw-r--r--app/models/application_setting.rb11
-rw-r--r--app/models/ci/build.rb28
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/commit_status.rb9
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/deployment.rb6
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/namespace.rb6
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/pool_repository.rb22
-rw-r--r--app/models/postgresql/replication_slot.rb11
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/shard.rb26
-rw-r--r--app/models/user.rb6
-rw-r--r--app/models/user_preference.rb5
-rw-r--r--app/policies/ci/build_policy.rb7
-rw-r--r--app/policies/deployment_policy.rb9
-rw-r--r--app/presenters/commit_status_presenter.rb5
-rw-r--r--app/serializers/deployment_entity.rb1
-rw-r--r--app/serializers/job_entity.rb1
-rw-r--r--app/serializers/user_preference_entity.rb4
-rw-r--r--app/services/ci/register_job_service.rb5
-rw-r--r--app/services/merge_requests/refresh_service.rb11
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml8
-rw-r--r--app/views/groups/labels/index.html.haml2
-rw-r--r--app/views/projects/deployments/_rollback.haml2
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml4
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml18
-rw-r--r--app/views/projects/tree/_blob_item.html.haml12
-rw-r--r--app/views/projects/tree/_spinner.html.haml3
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml6
-rw-r--r--app/views/projects/tree/_tree_item.html.haml9
-rw-r--r--app/views/projects/tree/_tree_row.html.haml33
-rw-r--r--app/views/shared/empty_states/_labels.html.haml9
-rw-r--r--changelogs/unreleased/51259-ci-cd-gitlab-ui.yml5
-rw-r--r--changelogs/unreleased/52300-pool-repositories.yml5
-rw-r--r--changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml5
-rw-r--r--changelogs/unreleased/53362-allow-concurrency-in-puma.yml5
-rw-r--r--changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml5
-rw-r--r--changelogs/unreleased/disallow-retry-of-old-builds.yml5
-rw-r--r--changelogs/unreleased/fast_project_blob_path.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml5
-rw-r--r--changelogs/unreleased/gl-ui-tooltip.yml5
-rw-r--r--changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml5
-rw-r--r--changelogs/unreleased/issue_51323.yml5
-rw-r--r--changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml5
-rw-r--r--changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-52176.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml5
-rw-r--r--changelogs/unreleased/toggle-sidebar-alignment.yml5
-rw-r--r--changelogs/unreleased/update_license_management_job.yml5
-rw-r--r--changelogs/unreleased/zj-bump-gitaly-0-128.yml5
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/initializers/fill_shards.rb4
-rw-r--r--db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb18
-rw-r--r--db/migrate/20181019032400_add_shards_table.rb11
-rw-r--r--db/migrate/20181019032408_add_repositories_table.rb15
-rw-r--r--db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb22
-rw-r--r--db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb11
-rw-r--r--db/schema.rb20
-rw-r--r--doc/ci/services/mysql.md2
-rw-r--r--doc/development/code_review.md6
-rw-r--r--doc/development/contributing/merge_request_workflow.md3
-rw-r--r--doc/user/permissions.md5
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/gitlab/ci/status/build/action.rb2
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/build/common.rb2
-rw-r--r--lib/gitlab/ci/status/build/created.rb2
-rw-r--r--lib/gitlab/ci/status/build/erased.rb2
-rw-r--r--lib/gitlab/ci/status/build/factory.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed.rb5
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/manual.rb2
-rw-r--r--lib/gitlab/ci/status/build/pending.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retried.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/scheduled.rb2
-rw-r--r--lib/gitlab/ci/status/build/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/build/unschedule.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/extended.rb2
-rw-r--r--lib/gitlab/ci/status/external/common.rb2
-rw-r--r--lib/gitlab/ci/status/external/factory.rb2
-rw-r--r--lib/gitlab/ci/status/factory.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/group/common.rb2
-rw-r--r--lib/gitlab/ci/status/group/factory.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/blocked.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/delayed.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/scheduled.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/stage/common.rb2
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb15
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb2
-rw-r--r--lib/gitlab/ci/trace/stream.rb5
-rw-r--r--lib/gitlab/ci/variables/collection.rb2
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/kubernetes/helm.rb1
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb6
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb2
-rw-r--r--locale/gitlab.pot5
-rw-r--r--qa/qa.rb60
-rw-r--r--qa/qa/factory/README.md410
-rw-r--r--qa/qa/factory/resource/branch.rb77
-rw-r--r--qa/qa/factory/resource/ci_variable.rb30
-rw-r--r--qa/qa/factory/resource/deploy_key.rb43
-rw-r--r--qa/qa/factory/resource/deploy_token.rb50
-rw-r--r--qa/qa/factory/resource/file.rb38
-rw-r--r--qa/qa/factory/resource/fork.rb70
-rw-r--r--qa/qa/factory/resource/group.rb68
-rw-r--r--qa/qa/factory/resource/issue.rb30
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb57
-rw-r--r--qa/qa/factory/resource/label.rb39
-rw-r--r--qa/qa/factory/resource/merge_request.rb71
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb31
-rw-r--r--qa/qa/factory/resource/personal_access_token.rb27
-rw-r--r--qa/qa/factory/resource/project.rb78
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb36
-rw-r--r--qa/qa/factory/resource/project_milestone.rb36
-rw-r--r--qa/qa/factory/resource/runner.rb49
-rw-r--r--qa/qa/factory/resource/sandbox.rb60
-rw-r--r--qa/qa/factory/resource/ssh_key.rb28
-rw-r--r--qa/qa/factory/resource/user.rb56
-rw-r--r--qa/qa/factory/resource/wiki.rb30
-rw-r--r--qa/qa/page/main/login.rb2
-rw-r--r--qa/qa/page/merge_request/show.rb57
-rw-r--r--qa/qa/resource/README.md392
-rw-r--r--qa/qa/resource/api_fabricator.rb (renamed from qa/qa/factory/api_fabricator.rb)16
-rw-r--r--qa/qa/resource/base.rb (renamed from qa/qa/factory/base.rb)30
-rw-r--r--qa/qa/resource/branch.rb77
-rw-r--r--qa/qa/resource/ci_variable.rb30
-rw-r--r--qa/qa/resource/deploy_key.rb43
-rw-r--r--qa/qa/resource/deploy_token.rb50
-rw-r--r--qa/qa/resource/file.rb36
-rw-r--r--qa/qa/resource/fork.rb43
-rw-r--r--qa/qa/resource/group.rb68
-rw-r--r--qa/qa/resource/issue.rb30
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb57
-rw-r--r--qa/qa/resource/label.rb39
-rw-r--r--qa/qa/resource/merge_request.rb71
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb31
-rw-r--r--qa/qa/resource/personal_access_token.rb27
-rw-r--r--qa/qa/resource/project.rb80
-rw-r--r--qa/qa/resource/project_imported_from_github.rb36
-rw-r--r--qa/qa/resource/project_milestone.rb36
-rw-r--r--qa/qa/resource/repository/project_push.rb (renamed from qa/qa/factory/repository/project_push.rb)8
-rw-r--r--qa/qa/resource/repository/push.rb (renamed from qa/qa/factory/repository/push.rb)6
-rw-r--r--qa/qa/resource/repository/wiki_push.rb (renamed from qa/qa/factory/repository/wiki_push.rb)8
-rw-r--r--qa/qa/resource/runner.rb49
-rw-r--r--qa/qa/resource/sandbox.rb60
-rw-r--r--qa/qa/resource/settings/hashed_storage.rb (renamed from qa/qa/factory/settings/hashed_storage.rb)6
-rw-r--r--qa/qa/resource/ssh_key.rb26
-rw-r--r--qa/qa/resource/user.rb92
-rw-r--r--qa/qa/resource/wiki.rb30
-rw-r--r--qa/qa/runtime/api/client.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb8
-rw-r--r--qa/spec/factory/resource/user_spec.rb36
-rw-r--r--qa/spec/resource/api_fabricator_spec.rb (renamed from qa/spec/factory/api_fabricator_spec.rb)46
-rw-r--r--qa/spec/resource/base_spec.rb (renamed from qa/spec/factory/base_spec.rb)104
-rw-r--r--qa/spec/resource/repository/push_spec.rb (renamed from qa/spec/factory/repository/push_spec.rb)2
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb7
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb5
-rw-r--r--spec/factories/ci/builds.rb6
-rw-r--r--spec/features/projects/environments/environment_spec.rb13
-rw-r--r--spec/features/projects/environments/environments_spec.rb67
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb57
-rw-r--r--spec/finders/notes_finder_spec.rb10
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/deployment.json4
-rw-r--r--spec/fixtures/api/schemas/job/job.json6
-rw-r--r--spec/helpers/tree_helper_spec.rb45
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_form_spec.js32
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js126
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js24
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js7
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js13
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js10
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb20
-rw-r--r--spec/models/ci/build_spec.rb118
-rw-r--r--spec/models/ci/pipeline_spec.rb13
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb16
-rw-r--r--spec/models/merge_request_diff_spec.rb21
-rw-r--r--spec/models/merge_request_spec.rb26
-rw-r--r--spec/models/namespace_spec.rb11
-rw-r--r--spec/models/postgresql/replication_slot_spec.rb20
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/models/shard_spec.rb50
-rw-r--r--spec/models/user_preference_spec.rb37
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb2
-rw-r--r--spec/serializers/deployment_entity_spec.rb22
-rw-r--r--spec/services/ci/register_job_service_spec.rb14
-rw-r--r--spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb6
-rw-r--r--spec/views/projects/tree/_tree_row.html.haml_spec.rb (renamed from spec/views/projects/tree/_blob_item.html.haml_spec.rb)9
292 files changed, 3226 insertions, 1978 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 241dcaa7832..6e5296e231e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.4.5 (2018-11-04)
+
+### Fixed (4 changes, 1 of them is from the community)
+
+- fix link to enable usage ping from convdev index. !22545 (Anand Capur)
+- Update gitlab-ui dependency to 1.8.0-hotfix.1 to fix IE11 bug.
+- Remove duplicate escape in job sidebar.
+- Fixed merge request fill tree toggling not respecting fluid width preference.
+
+### Other (1 change)
+
+- Fix stage dropdown not rendering in different languages.
+
+
## 11.4.4 (2018-10-30)
### Security (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index bcc9c2840a7..4c2a8041846 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.126.0
+0.128.0
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 82a191d056b..6c18a0fd390 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -1,6 +1,4 @@
import Vue from 'vue';
-import { GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
Vue.component('gl-loading-icon', GlLoadingIcon);
-
-Vue.directive('gl-tooltip', GlTooltipDirective);
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 1b59777f901..254bc235691 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
@@ -10,6 +11,9 @@ export default {
Icon,
UserAvatarImage,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
discussions: {
type: Array,
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 6eff3013dcd..f4a9be19496 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -167,7 +167,7 @@ export default {
<button
v-if="shouldShowCommentButton"
type="button"
- class="add-diff-note js-add-diff-note-button"
+ class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
@click="handleCommentButton"
>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 62fa34e835a..542acd3d930 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -102,7 +102,7 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
- class="diff-line-num new_line"
+ class="diff-line-num new_line qa-new-diff-line"
/>
<td
:class="line.type"
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 5bea47f23c5..d8d0fa1fac4 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -31,7 +31,7 @@ class DirtySubmitForm {
updateDirtyInput(event) {
const input = event.target;
- if (!input.dataset.dirtySubmitOriginalValue) return;
+ if (!input.dataset.isDirtySubmitInput) return;
this.updateDirtyInputs(input);
this.toggleSubmission();
@@ -65,6 +65,7 @@ class DirtySubmitForm {
}
static initInput(element) {
+ element.dataset.isDirtySubmitInput = true;
element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element);
}
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 2bc168a6b02..0a3ae384afa 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,4 +1,6 @@
<script>
+import { s__, sprintf } from '~/locale';
+import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -28,10 +30,24 @@ export default {
},
},
methods: {
- onClickAction(endpoint) {
+ onClickAction(action) {
+ if (action.scheduledAt) {
+ const confirmationMessage = sprintf(
+ s__(
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ ),
+ { jobName: action.name },
+ );
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(confirmationMessage)) {
+ return;
+ }
+ }
+
this.isLoading = true;
- eventHub.$emit('postAction', { endpoint });
+ eventHub.$emit('postAction', { endpoint: action.playPath });
},
isActionDisabled(action) {
@@ -41,6 +57,11 @@ export default {
return !action.playable;
},
+
+ remainingTime(action) {
+ const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now();
+ return formatTime(Math.max(0, remainingMilliseconds));
+ },
},
};
</script>
@@ -54,7 +75,7 @@ export default {
:aria-label="title"
:disabled="isLoading"
type="button"
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
>
@@ -75,12 +96,19 @@ export default {
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
type="button"
- class="js-manual-action-link no-btn btn"
- @click="onClickAction(action.play_path)"
+ class="js-manual-action-link no-btn btn d-flex align-items-center"
+ @click="onClickAction(action)"
>
- <span>
+ <span class="flex-fill">
{{ action.name }}
</span>
+ <span
+ v-if="action.scheduledAt"
+ class="text-secondary"
+ >
+ <icon name="clock" />
+ {{ remainingTime(action) }}
+ </span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index b62a5bb1940..41f59447905 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -13,6 +13,7 @@ import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
/**
* Environment Item Component
@@ -74,21 +75,6 @@ export default {
},
/**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return (
- this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0
- );
- },
-
- /**
* Checkes whether the environment is protected.
* (`is_protected` currently only set in EE)
*
@@ -154,23 +140,20 @@ export default {
return '';
},
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map(action => {
- const parsedAction = {
- name: humanize(action.name),
- play_path: action.play_path,
- playable: action.playable,
- };
- return parsedAction;
- });
+ actions() {
+ if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
+ return [];
}
- return [];
+
+ const { manualActions, scheduledActions } = convertObjectPropsToCamelCase(
+ this.model.last_deployment,
+ { deep: true },
+ );
+ const combinedActions = (manualActions || []).concat(scheduledActions || []);
+ return combinedActions.map(action => ({
+ ...action,
+ name: humanize(action.name),
+ }));
},
/**
@@ -443,7 +426,7 @@ export default {
displayEnvironmentActions() {
return (
- this.hasManualActions ||
+ this.actions.length > 0 ||
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
@@ -619,8 +602,8 @@ export default {
/>
<actions-component
- v-if="hasManualActions && canCreateDeployment"
- :actions="manualActions"
+ v-if="actions.length > 0"
+ :actions="actions"
/>
<terminal-button-component
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 17fd5321642..93c89411b4a 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,10 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
mixins: [timeagoMixin],
props: {
@@ -53,16 +55,16 @@ export default {
class="btn-group d-flex"
role="group"
>
- <a
+ <gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
>
{{ s__('Job|Keep') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.download_path"
:href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default"
@@ -70,15 +72,15 @@ export default {
rel="nofollow"
>
{{ s__('Job|Download') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
>
{{ s__('Job|Browse') }}
- </a>
+ </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 7d51f6afd10..06fe23fedce 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
+ GlLink,
},
props: {
commit: {
@@ -31,10 +33,10 @@ export default {
<p>
{{ __('Commit') }}
- <a
+ <gl-link
:href="commit.commit_path"
class="js-commit-sha commit-sha link-commit"
- >{{ commit.short_id }}</a>
+ >{{ commit.short_id }}</gl-link>
<clipboard-button
:text="commit.short_id"
@@ -42,11 +44,11 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <a
+ <gl-link
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</a>
+ >!{{ mergeRequest.iid }}</gl-link>
</p>
<p class="build-light-text append-bottom-0">
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index ee5ceb99b0a..be7425c2d25 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -1,5 +1,10 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlLink,
+ },
props: {
illustrationPath: {
type: String,
@@ -62,13 +67,13 @@ export default {
v-if="action"
class="text-center"
>
- <a
+ <gl-link
:href="action.path"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
{{ action.button_title }}
- </a>
+ </gl-link>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index 5ffbfb6e19a..d80e905c68e 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
props: {
user: {
@@ -29,9 +31,9 @@ export default {
<div class="erased alert alert-warning">
<template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }}
- <a :href="user.web_url">
+ <gl-link :href="user.web_url">
{{ user.username }}
- </a>
+ </gl-link>
</template>
<template v-else>
{{ s__("Job|Job has been erased") }}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 3cabbfc6e27..6e95e3d16f8 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,6 +1,7 @@
<script>
import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
@@ -23,6 +24,7 @@ export default {
EmptyState,
EnvironmentsBlock,
ErasedBlock,
+ GlLoadingIcon,
Log,
LogTopBar,
StuckBlock,
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 6486b25c8a7..cdac8a391d1 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,15 +1,16 @@
<script>
+import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
CiIcon,
Icon,
+ GlLink,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
job: {
@@ -37,11 +38,10 @@ export default {
active: isActive
}"
>
- <a
- v-tooltip
+ <gl-link
+ v-gl-tooltip
:href="job.status.details_path"
:title="tooltipText"
- data-container="body"
data-boundary="viewport"
class="js-job-link"
>
@@ -60,6 +60,6 @@ export default {
name="retry"
class="js-retry-icon"
/>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 94ab1b16c84..eeefa33264f 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
Icon,
+ GlLink,
+ GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
scrollDown,
props: {
@@ -73,76 +75,70 @@ export default {
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
- <a
+ <gl-link
v-if="rawPath"
:href="rawPath"
class="js-raw-link raw-link"
>
{{ s__("Job|Complete Raw") }}
- </a>
+ </gl-link>
</template>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
- <a
+ <gl-link
v-if="rawPath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="js-raw-link-controller controllers-buttons"
- data-container="body"
>
<icon name="doc-text" />
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="erasePath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Erase job log')"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
- data-container="body"
data-method="post"
>
<icon name="remove" />
- </a>
+ </gl-link>
<!-- eo links -->
<!-- scroll buttons -->
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to top')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollTopDisabled"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
>
- <icon name="scroll_up"/>
- </button>
+ <icon name="scroll_up" />
+ </gl-button>
</div>
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to bottom')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollBottomDisabled"
- type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
- >
- </button>
+ />
</div>
<!-- eo scroll buttons -->
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index aeafe98a70b..cfedb38e17a 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,6 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'SidebarDetailRow',
+ components: {
+ GlLink,
+ },
props: {
title: {
type: String,
@@ -41,7 +46,7 @@ export default {
v-if="hasHelpURL"
class="help-button float-right"
>
- <a
+ <gl-link
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -50,7 +55,7 @@ export default {
class="fa fa-question-circle"
aria-hidden="true"
></i>
- </a>
+ </gl-link>
</span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 1d5789b175a..ca4bf471363 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,8 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
+ components: {
+ GlLink,
+ },
props: {
hasNoRunnersForProject: {
type: Boolean,
@@ -52,12 +56,12 @@ export default {
</p>
{{ __("Go to") }}
- <a
+ <gl-link
v-if="runnersPath"
:href="runnersPath"
class="js-runners-path"
>
{{ __("Runners page") }}
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index b980e43b898..554db102027 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
@@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
type="button"
- class="btn btn-transparent"
+ class="btn btn-transparent qa-discussion-option"
@click.prevent="setNoteType('discussion')">
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 6e8f43048d1..affa2d1b574 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,7 +1,8 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
import { mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
export default {
components: {
@@ -12,14 +13,17 @@ export default {
type: Array,
required: true,
},
- defaultValue: {
+ selectedValue: {
type: Number,
default: null,
required: false,
},
},
data() {
- return { currentValue: this.defaultValue };
+ return {
+ currentValue: this.selectedValue,
+ defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ };
},
computed: {
...mapGetters(['getNotesDataByProp']),
@@ -28,8 +32,11 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue);
},
},
+ mounted() {
+ this.toggleCommentsForm();
+ },
methods: {
- ...mapActions(['filterDiscussion']),
+ ...mapActions(['filterDiscussion', 'setCommentsDisabled']),
selectFilter(value) {
const filter = parseInt(value, 10);
@@ -39,6 +46,10 @@ export default {
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
+ this.toggleCommentsForm();
+ },
+ toggleCommentsForm() {
+ this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
},
};
@@ -73,6 +84,10 @@ export default {
>
{{ filter.title }}
</button>
+ <div
+ v-if="filter.value === defaultValue"
+ class="dropdown-divider"
+ ></div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 38c43e5fe08..31ee8fed984 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -187,7 +187,7 @@ export default {
:data-supports-quick-actions="!isEditing"
name="note[note]"
class="note-textarea js-gfm-input js-note-text
-js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
+js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index c5fdfa1d47c..6293dd5b7e1 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -369,7 +369,7 @@ Please check your network connection and try again.`;
role="group">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-2"
+ class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
title="Add a reply"
@click="showReplyForm">Reply...</button>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7514ce8a1eb..ed5ac112dc0 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -60,6 +60,7 @@ export default {
'getNotesDataByProp',
'discussionCount',
'isLoading',
+ 'commentsDisabled',
]),
noteableType() {
return this.noteableData.noteableType;
@@ -206,6 +207,7 @@ export default {
</ul>
<comment-form
+ v-if="!commentsDisabled"
:noteable-type="noteableType"
:markdown-version="markdownVersion"
/>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 2c3e07c0506..3147dc64c27 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
+export const HISTORY_ONLY_FILTER_VALUE = 2;
+export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
index 06eadaeea0e..5c5f38a3fb0 100644
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -6,7 +6,7 @@ export default store => {
if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
+ const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
const filters = Object.keys(filterValues).map(entry => ({
title: entry,
@@ -24,7 +24,7 @@ export default store => {
return createElement('discussion-filter', {
props: {
filters,
- defaultValue,
+ selectedValue,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b5dd49bc6c9..88739ffb083 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => {
});
};
+export const setCommentsDisabled = ({ commit }, data) => {
+ commit(types.DISABLE_COMMENTS, data);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index e4f36154fcd..8df95c279eb 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
return getters.unresolvedDiscussionsIdsByDate[0];
};
+export const commentsDisabled = state => state.commentsDisabled;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 400142668ea..8aea269ea7d 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -21,6 +21,7 @@ export default () => ({
noteableData: {
current_user: {},
},
+ commentsDisabled: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2fa53aef1d4..dfbf3b7b34b 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
+export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 65085452139..c8d9e196103 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -225,4 +225,8 @@ export default {
discussion.truncated_diff_lines = diffLines;
},
+
+ [types.DISABLE_COMMENTS](state, value) {
+ state.commentsDisabled = value;
+ },
};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index a7507fb3b6f..07a4af3e61e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -29,7 +29,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.",
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
),
{ jobName: action.name },
);
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 8950ae31627..4d461baf74d 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -5,7 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import { GlModal } from '@gitlab-org/gitlab-ui';
+import { GlModal, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
@@ -16,6 +16,9 @@ export default {
Icon,
GlModal,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
currentEmoji: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 9327a2a4a6c..a35986b2d03 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -1,10 +1,14 @@
<script>
import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
/**
* Counts down to a given end date.
*/
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
endDateString: {
type: String,
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f26b1fddae5..43b7c26b272 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -348,6 +348,7 @@
@include media-breakpoint-down(xs) {
width: 100%;
+ margin: $btn-side-margin 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 2e7f25d975e..6f103e4e89a 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -322,15 +322,15 @@
width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
+ height: $toggle-sidebar-height;
bottom: 0;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
- line-height: 1;
svg {
margin-right: 8px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index c030d75f5a4..9837b1a6bd0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -291,7 +291,7 @@
/*
* Mixin that handles the position of the controls placed on the top bar
*/
-@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) {
+@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
display: flex;
font-size: $control-font-size;
justify-content: $flex-direction;
@@ -304,8 +304,9 @@
svg {
width: 15px;
height: 15px;
- display: block;
+ display: $svg-display;
fill: $gl-text-color;
+ top: $svg-top;
}
.controllers-buttons {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 339388392df..6954e6599b1 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -147,3 +147,9 @@ table {
}
}
}
+
+.top-area + .content-list {
+ th {
+ border-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ad66a0365ed..19eee4e4aba 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
+$toggle-sidebar-height: 48px;
/*
* Color schema
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 31b258e56dd..1449723de52 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -94,7 +94,7 @@
}
.controllers {
- @include build-controllers(15px, center, false, 0);
+ @include build-controllers(15px, center, false, 0, inline, 0);
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 19a36061c45..347fcad771a 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -44,11 +44,6 @@
margin: 0;
}
- .icon-play {
- height: 13px;
- width: 12px;
- }
-
.external-url,
.dropdown-new {
color: $gl-text-color-secondary;
@@ -366,7 +361,7 @@
}
.arrow-shadow {
- content: "";
+ content: '';
position: absolute;
width: 7px;
height: 7px;
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 86e70955389..617b3db2fae 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -39,10 +39,6 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- }
}
.next-run-cell {
@@ -52,6 +48,10 @@
a {
color: $text-color;
}
+
+ svg {
+ vertical-align: middle;
+ }
}
.pipeline-schedules-user-callout {
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 81fd3b7a547..bd95dcd323f 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -3,7 +3,7 @@
class PersonalAccessTokensFinder
attr_accessor :params
- delegate :build, :find, :find_by, :find_by_token, to: :execute
+ delegate :build, :find, :find_by_id, :find_by_token, to: :execute
def initialize(params = {})
@params = params
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 0c9f69b6714..9a1c2a4c9e1 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :archive_builds_in_human_readable,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 910c9e9446f..b0f63de2fb8 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -109,6 +109,8 @@ module IconsHelper
def file_type_icon_class(type, mode, name)
if type == 'folder'
icon_class = 'folder'
+ elsif type == 'archive'
+ icon_class = 'archive'
elsif mode == '120000'
icon_class = 'share'
else
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 6d2da5699fb..78a11616d4c 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -31,11 +31,21 @@ module TreeHelper
# mode - File unix mode
# name - File name
def tree_icon(type, mode, name)
- icon("#{file_type_icon_class(type, mode, name)} fw")
+ icon([file_type_icon_class(type, mode, name), 'fw'])
end
- def tree_hex_class(content)
- "file_#{hexdigest(content.name)}"
+ # Using Rails `*_path` methods can be slow, especially when generating
+ # many paths, as with a repository tree that has thousands of items.
+ def fast_project_blob_path(project, blob_path)
+ Addressable::URI.escape(
+ File.join(relative_url_root, project.path_with_namespace, 'blob', blob_path)
+ )
+ end
+
+ def fast_project_tree_path(project, tree_path)
+ Addressable::URI.escape(
+ File.join(relative_url_root, project.path_with_namespace, 'tree', tree_path)
+ )
end
# Simple shortcut to File.join
@@ -142,4 +152,8 @@ module TreeHelper
def selected_branch
@branch_name || tree_edit_branch
end
+
+ def relative_url_root
+ Gitlab.config.gitlab.relative_url_root.presence || '/'
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index b66ec0ffab6..704310f53f0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField
include TokenAuthenticatable
include IgnorableColumn
+ include ChronicDurationAttribute
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base
default_value_for :id, 1
+ chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
+
validates :uuid, presence: true
validates :session_expire_delay,
@@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true
+ validates :archive_builds_in_seconds,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base
latest_terms
end
+ def archive_builds_older_than
+ archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
+ end
+
private
def ensure_uuid!
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bb5d52fc78d..d7eab57763e 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -245,17 +245,37 @@ module Ci
.fabricate!
end
- def other_actions
+ def other_manual_actions
pipeline.manual_actions.where.not(name: name)
end
+ def other_scheduled_actions
+ pipeline.scheduled_actions.where.not(name: name)
+ end
+
def pages_generator?
Gitlab.config.pages.enabled &&
self.name == 'pages'
end
+ # degenerated build is one that cannot be run by Runner
+ def degenerated?
+ self.options.nil?
+ end
+
+ def degenerate!
+ self.update!(options: nil, yaml_variables: nil, commands: nil)
+ end
+
+ def archived?
+ return true if degenerated?
+
+ archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
+ archive_builds_older_than.present? && created_at < archive_builds_older_than
+ end
+
def playable?
- action? && (manual? || scheduled? || retryable?)
+ action? && !archived? && (manual? || scheduled? || retryable?)
end
def schedulable?
@@ -283,7 +303,7 @@ module Ci
end
def retryable?
- success? || failed? || canceled?
+ !archived? && (success? || failed? || canceled?)
end
def retries_count
@@ -291,7 +311,7 @@ module Ci
end
def retries_max
- self.options.fetch(:retry, 0).to_i
+ self.options.to_h.fetch(:retry, 0).to_i
end
def latest?
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 34a889057ab..11c88200c37 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -15,7 +15,7 @@ module Ci
metadata: nil,
trace: nil,
junit: 'junit.xml',
- codequality: 'codequality.json',
+ codequality: 'gl-code-quality-report.json',
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index aeee7f0a5d2..0daf2419b67 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -238,6 +238,10 @@ module Ci
end
end
+ def self.latest_successful_ids_per_project
+ success.group(:project_id).select('max(id) as id')
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 95c88e11a6e..755f8bd4d06 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5,
runner_unsupported: 6,
stale_schedule: 7,
- job_execution_timeout: 8
+ job_execution_timeout: 8,
+ archived_failure: 9
}
##
@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base
false
end
- # To be overridden when inherrited from
def retryable?
false
end
- # To be overridden when inherrited from
def cancelable?
false
end
+ def archived?
+ false
+ end
+
def stuck?
false
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 0b2eedf3631..e3524305346 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
include PolicyActor
+ include Gitlab::Utils::StrongMemoize
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
@@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base
# to a single project, later we're going to extend
# that to be for multiple projects and namespaces.
def project
- projects.first
+ strong_memoize(:project) do
+ projects.first
+ end
end
def expires_at
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ee5b96e7454..37efbb04fce 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -55,7 +55,11 @@ class Deployment < ActiveRecord::Base
end
def manual_actions
- @manual_actions ||= deployable.try(:other_actions)
+ @manual_actions ||= deployable.try(:other_manual_actions)
+ end
+
+ def scheduled_actions
+ @scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
def includes_commit?(commit)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7eef08aa6a3..735d9fba966 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -353,6 +353,15 @@ class MergeRequest < ActiveRecord::Base
end
end
+ # Returns true if there are commits that match at least one commit SHA.
+ def includes_any_commits?(shas)
+ if persisted?
+ merge_request_diff.commits_by_shas(shas).exists?
+ else
+ (commit_shas & shas).present?
+ end
+ end
+
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 02c6b650f33..bb6ff8921df 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base
merge_request_diff_commits.map(&:sha)
end
+ def commits_by_shas(shas)
+ return [] unless shas.present?
+
+ merge_request_diff_commits.where(sha: shas)
+ end
+
def diff_refs=(new_diff_refs)
self.base_commit_sha = new_diff_refs&.base_sha
self.start_commit_sha = new_diff_refs&.start_sha
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 74d48d0a9af..4a6627d3ca1 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path)
end
+ # Includes pipelines from this namespace and pipelines from all subgroups
+ # that belongs to this namespace
+ def all_pipelines
+ Ci::Pipeline.where(project: all_projects)
+ end
+
def has_parent?
parent.present?
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 990689a95f5..592efb714f3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -117,6 +117,8 @@ class Note < ActiveRecord::Base
case notes_filter
when UserPreference::NOTES_FILTERS[:only_comments]
user
+ when UserPreference::NOTES_FILTERS[:only_activity]
+ system
else
all
end
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
new file mode 100644
index 00000000000..8ef74539209
--- /dev/null
+++ b/app/models/pool_repository.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class PoolRepository < ActiveRecord::Base
+ POOL_PREFIX = '@pools'
+
+ belongs_to :shard
+ validates :shard, presence: true
+
+ # For now, only pool repositories are tracked in the database. However, we may
+ # want to add other repository types in the future
+ self.table_name = 'repositories'
+
+ has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id
+
+ def shard_name
+ shard&.name
+ end
+
+ def shard_name=(name)
+ self.shard = Shard.by_name(name)
+ end
+end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index 70c7432e6b5..e264fe88e47 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -4,6 +4,15 @@ module Postgresql
class ReplicationSlot < ActiveRecord::Base
self.table_name = 'pg_replication_slots'
+ # Returns true if there are any replication slots in use.
+ # PostgreSQL-compatible databases such as Aurora don't support
+ # replication slots, so this will return false as well.
+ def self.in_use?
+ transaction { exists? }
+ rescue ActiveRecord::StatementInvalid
+ false
+ end
+
# Returns true if the lag observed across all replication slots exceeds a
# given threshold.
#
@@ -11,6 +20,8 @@ module Postgresql
# statistics it takes between 1 and 5 seconds to replicate around
# 100 MB of data.
def self.lag_too_great?(max = 100.megabytes)
+ return false unless in_use?
+
lag_function = "#{Gitlab::Database.pg_wal_lsn_diff}" \
"(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)::bigint"
diff --git a/app/models/project.rb b/app/models/project.rb
index fa995b5b061..d3b148d0ac0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -95,8 +95,7 @@ class Project < ActiveRecord::Base
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
- after_create :set_last_activity_at
- after_create :set_last_repository_updated_at
+ after_create :set_timestamps_for_create
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
@@ -124,6 +123,7 @@ class Project < ActiveRecord::Base
alias_attribute :title, :name
# Relations
+ belongs_to :pool_repository
belongs_to :creator, class_name: 'User'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
belongs_to :namespace
@@ -2102,13 +2102,8 @@ class Project < ActiveRecord::Base
gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
end
- # set last_activity_at to the same as created_at
- def set_last_activity_at
- update_column(:last_activity_at, self.created_at)
- end
-
- def set_last_repository_updated_at
- update_column(:last_repository_updated_at, self.created_at)
+ def set_timestamps_for_create
+ update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at)
end
def cross_namespace_reference?(from)
diff --git a/app/models/shard.rb b/app/models/shard.rb
new file mode 100644
index 00000000000..2fa22bd040c
--- /dev/null
+++ b/app/models/shard.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class Shard < ActiveRecord::Base
+ # Store shard names from the configuration file in the database. This is not a
+ # list of active shards - we just want to assign an immutable, unique ID to
+ # every shard name for easy indexing / referencing.
+ def self.populate!
+ return unless table_exists?
+
+ # The GitLab config does not change for the lifecycle of the process
+ in_config = Gitlab.config.repositories.storages.keys.map(&:to_s)
+
+ transaction do
+ in_db = all.pluck(:name)
+ missing = in_config - in_db
+
+ missing.map { |name| by_name(name) }
+ end
+ end
+
+ def self.by_name(name)
+ find_or_create_by(name: name)
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index d3eb7162174..039a3854edb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -460,12 +460,6 @@ class User < ActiveRecord::Base
by_username(username).take!
end
- def find_by_personal_access_token(token_string)
- return unless token_string
-
- PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
Key.find_by(id: key_id)&.user
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 6cd91abc261..32d0407800f 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base
# We could use enums, but Rails 4 doesn't support multiple
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
- NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze
+ NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
belongs_to :user
@@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base
def notes_filters
{
s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes],
- s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments]
+ s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments],
+ s_('Notes|Show history only') => NOTES_FILTERS[:only_activity]
}
end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 3858b29c82c..0ca3e696f46 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -20,12 +20,17 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:archived, scope: :subject) do
+ @subject.archived?
+ end
+
condition(:terminal, scope: :subject) do
@subject.has_terminal?
end
- rule { protected_ref }.policy do
+ rule { protected_ref | archived }.policy do
prevent :update_build
+ prevent :update_commit_status
prevent :erase_build
end
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
index 56ac898b6ab..d4f2f3c52b1 100644
--- a/app/policies/deployment_policy.rb
+++ b/app/policies/deployment_policy.rb
@@ -2,4 +2,13 @@
class DeploymentPolicy < BasePolicy
delegate { @subject.project }
+
+ condition(:can_retry_deployable) do
+ can?(:update_build, @subject.deployable)
+ end
+
+ rule { ~can_retry_deployable }.policy do
+ prevent :create_deployment
+ prevent :update_deployment
+ end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index a866e76df5a..0cd77da6303 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner',
stale_schedule: 'Delayed job could not be executed by some reason, please try again',
- job_execution_timeout: 'The script exceeded the maximum execution time set for the job'
+ job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
+ archived_failure: 'The job is archived and cannot be run'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end
def unrecoverable?
- script_failure? || missing_dependency_failure?
+ script_failure? || missing_dependency_failure? || archived_failure?
end
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 344148a1fb7..aa1d9e6292c 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity
expose :deployable, using: JobEntity
expose :manual_actions, using: JobEntity
+ expose :scheduled_actions, using: JobEntity
end
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index aebbc18e32f..d0099ae77f2 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity
expose :name
expose :started?, as: :started
+ expose :archived?, as: :archived
expose :build_path do |build|
build_path(build)
diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb
index fbdaab459b3..b99f80424db 100644
--- a/app/serializers/user_preference_entity.rb
+++ b/app/serializers/user_preference_entity.rb
@@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity
expose :notes_filters do |user_preference|
UserPreference.notes_filters
end
+
+ expose :default_notes_filter do |user_preference|
+ UserPreference::NOTES_FILTERS[:all_notes]
+ end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 5a7be921389..e06f1c05843 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -82,6 +82,11 @@ module Ci
return false
end
+ if build.archived?
+ build.drop!(:archived_failure)
+ return false
+ end
+
build.run!
true
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index f01872b205e..53768ff2cbe 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -87,11 +87,8 @@ module MergeRequests
filter_merge_requests(merge_requests).each do |merge_request|
if branch_and_project_match?(merge_request) || @push.force_push?
merge_request.reload_diff(current_user)
- else
- mr_commit_ids = merge_request.commit_shas
- push_commit_ids = @commits.map(&:id)
- matches = mr_commit_ids & push_commit_ids
- merge_request.reload_diff(current_user) if matches.any?
+ elsif merge_request.includes_any_commits?(push_commit_ids)
+ merge_request.reload_diff(current_user)
end
merge_request.mark_as_unchecked
@@ -104,6 +101,10 @@ module MergeRequests
end
# rubocop: enable CodeReuse/ActiveRecord
+ def push_commit_ids
+ @push_commit_ids ||= @commits.map(&:id)
+ end
+
def branch_and_project_match?(merge_request)
merge_request.source_project == @project &&
merge_request.source_branch == @push.branch_name
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 97be658cd34..adb496495d1 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -41,5 +41,13 @@
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+ .form-group
+ = f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold'
+ = f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
+ .form-text.text-muted
+ Set the duration when build gonna be considered old. Archived builds cannot be retried.
+ Make it empty to never expire builds. It has to be larger than 1 day.
+ The default unit is in seconds, but you can define an alternative. For example:
+ <code>4 mins 2 sec</code>, <code>2h42min</code>.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 5b78ce910b8..4df3d831942 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -6,7 +6,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
-- if can_admin_label
+- if @labels.present? && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 281e042c915..1bd538a08ff 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+- if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last?
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 06ee883d6dc..2c6484c2c99 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -5,7 +5,7 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-- if can_admin_label
+- if @labels.present? && can_admin_label
- content_for(:header_content) do
.nav-controls
= link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 5d1bbb077af..515499956a2 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -34,7 +34,7 @@
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
- %li.notes-tab
+ %li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge.badge-pill= @merge_request.related_notes.user.count
@@ -48,7 +48,7 @@
= tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
- %li.diffs-tab
+ %li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 95bba47802c..66e202103a9 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -61,12 +61,14 @@
%td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
= build.present.callout_failure_message
%td.responsive-table-cell.build-actions
- = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
- = icon('repeat')
- %tr.build-trace-row.responsive-table-border-end
- %td
- %td.responsive-table-cell.build-trace-container{ colspan: 4 }
- %pre.build-trace.build-trace-rounded
- %code.bash.js-build-output
- = build_summary(build)
+ - if can?(current_user, :update_build, job)
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
+ = icon('repeat')
+ - if can?(current_user, :read_build, job)
+ %tr.build-trace-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-trace-container{ colspan: 4 }
+ %pre.build-trace.build-trace-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
deleted file mode 100644
index f79f3af36d4..00000000000
--- a/app/views/projects/tree/_blob_item.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id)
-%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
- %td.tree-item-file-name
- = tree_icon(type, blob_item.mode, blob_item.name)
- - file_name = blob_item.name
- = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
- %span= file_name
- - if is_lfs_blob
- %span.badge.label-lfs.prepend-left-5 LFS
- %td.d-none.d-sm-table-cell.tree-commit
- %td.tree-time-ago.cgray.text-right
- = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_spinner.html.haml b/app/views/projects/tree/_spinner.html.haml
deleted file mode 100644
index b47ad0f41e4..00000000000
--- a/app/views/projects/tree/_spinner.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%span.log_loading.hide
- %i.fa.fa-spinner.fa-spin
- Loading commit data...
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
deleted file mode 100644
index e563c8c4036..00000000000
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%tr.tree-item
- %td.tree-item-file-name
- %i.fa.fa-archive.fa-fw
- = submodule_link(submodule_item, @ref)
- %td
- %td.d-none.d-sm-table-cell
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
deleted file mode 100644
index ce0cd95b468..00000000000
--- a/app/views/projects/tree/_tree_item.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
- %td.tree-item-file-name
- = tree_icon(type, tree_item.mode, tree_item.name)
- - path = flatten_tree(@path, tree_item)
- = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do
- %span= path
- %td.d-none.d-sm-table-cell.tree-commit
- %td.tree-time-ago.text-right
- = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml
index 0a5c6f048f7..8a27ea66523 100644
--- a/app/views/projects/tree/_tree_row.html.haml
+++ b/app/views/projects/tree/_tree_row.html.haml
@@ -1,6 +1,27 @@
-- if tree_row.type == :tree
- = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' }
-- elsif tree_row.type == :blob
- = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' }
-- elsif tree_row.type == :commit
- = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item'
+- tree_row_name = tree_row.name
+- tree_row_type = tree_row.type
+
+%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" }
+ %td.tree-item-file-name
+ - if tree_row_type == :tree
+ = tree_icon('folder', tree_row.mode, tree_row.name)
+ - path = flatten_tree(@path, tree_row)
+ %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path }
+ %span= path
+
+ - elsif tree_row_type == :blob
+ = tree_icon('file', tree_row.mode, tree_row_name)
+ %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name }
+ %span= tree_row_name
+ - if @lfs_blob_ids.include?(tree_row.id)
+ %span.badge.label-lfs.prepend-left-5 LFS
+
+ - elsif tree_row_type == :commit
+ = tree_icon('archive', tree_row.mode, tree_row.name)
+ = submodule_link(tree_row, @ref)
+
+ %td.d-none.d-sm-table-cell.tree-commit
+ %td.tree-time-ago.text-right
+ %span.log_loading.hide
+ %i.fa.fa-spinner.fa-spin
+ Loading commit data...
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index b629ceafeb3..9133ce8ed22 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -6,6 +6,9 @@
.text-content
%h4= _("Labels can be applied to issues and merge requests to categorize them.")
%p= _("You can also star a label to make it a priority label.")
- - if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
- = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
+ .text-center
+ - if can?(current_user, :admin_label, @project)
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
+ = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
+ - if can?(current_user, :admin_label, @group)
+ = link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
new file mode 100644
index 00000000000..a15f1c033b3
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Uses gitlab-ui components in jobs components
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/52300-pool-repositories.yml b/changelogs/unreleased/52300-pool-repositories.yml
new file mode 100644
index 00000000000..5435f3aa21f
--- /dev/null
+++ b/changelogs/unreleased/52300-pool-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Start tracking shards and pool repositories in the database
+merge_request: 22482
+author:
+type: other
diff --git a/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml
new file mode 100644
index 00000000000..792b24d75ac
--- /dev/null
+++ b/changelogs/unreleased/52925-scheduled-pipelines-ui-problems.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing styling issues on the scheduled pipelines page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
deleted file mode 100644
index 605d3679159..00000000000
--- a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "fix link to enable usage ping from convdev index"
-merge_request: 22545
-author: Anand Capur
-type: fixed
diff --git a/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml
new file mode 100644
index 00000000000..d4d78a2fd06
--- /dev/null
+++ b/changelogs/unreleased/53230-remove_personal_access_tokens_finder_find_by_method.yml
@@ -0,0 +1,5 @@
+---
+title: Remove PersonalAccessTokensFinder#find_by method
+merge_request: 22617
+author:
+type: fixed
diff --git a/changelogs/unreleased/53362-allow-concurrency-in-puma.yml b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml
new file mode 100644
index 00000000000..5fbda0161c1
--- /dev/null
+++ b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Rails concurrency when running in Puma
+merge_request: 22751
+author:
+type: performance
diff --git a/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml
new file mode 100644
index 00000000000..cd9300ca2d1
--- /dev/null
+++ b/changelogs/unreleased/53450-wrong-value-for-kubernetes_version-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series
+merge_request: 22757
+author:
+type: other
diff --git a/changelogs/unreleased/disallow-retry-of-old-builds.yml b/changelogs/unreleased/disallow-retry-of-old-builds.yml
new file mode 100644
index 00000000000..03992fc0213
--- /dev/null
+++ b/changelogs/unreleased/disallow-retry-of-old-builds.yml
@@ -0,0 +1,5 @@
+---
+title: Soft-archive old jobs
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/fast_project_blob_path.yml b/changelogs/unreleased/fast_project_blob_path.yml
new file mode 100644
index 00000000000..b56c9d9cf59
--- /dev/null
+++ b/changelogs/unreleased/fast_project_blob_path.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of tree rendering in repositories with lots of items
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
new file mode 100644
index 00000000000..ecbfc323080
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/gl-ui-tooltip.yml b/changelogs/unreleased/gl-ui-tooltip.yml
new file mode 100644
index 00000000000..99ded9f812e
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's tooltip from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml
new file mode 100644
index 00000000000..d644ca86b79
--- /dev/null
+++ b/changelogs/unreleased/gt-update-project-and-group-labels-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Update project and group labels empty state
+merge_request: 22745
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/issue_51323.yml b/changelogs/unreleased/issue_51323.yml
new file mode 100644
index 00000000000..b0e83e303d1
--- /dev/null
+++ b/changelogs/unreleased/issue_51323.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'only history' option to notes filter
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml b/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
deleted file mode 100644
index b61f47724fc..00000000000
--- a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed merge request fill tree toggling not respecting fluid width preference
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml
new file mode 100644
index 00000000000..c89af78d989
--- /dev/null
+++ b/changelogs/unreleased/scheduled-manual-jobs-environment-play-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Add the Play button for delayed jobs in environment page
+merge_request: 22106
+author:
+type: added
diff --git a/changelogs/unreleased/sh-fix-issue-52176.yml b/changelogs/unreleased/sh-fix-issue-52176.yml
new file mode 100644
index 00000000000..7269e14d910
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-52176.yml
@@ -0,0 +1,5 @@
+---
+title: Disable replication lag check for Aurora PostgreSQL databases
+merge_request: 22786
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
new file mode 100644
index 00000000000..bea73f8d329
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize merge request refresh by using the database to check commit SHAs
+merge_request: 22731
+author:
+type: performance
diff --git a/changelogs/unreleased/toggle-sidebar-alignment.yml b/changelogs/unreleased/toggle-sidebar-alignment.yml
new file mode 100644
index 00000000000..428fe61da9b
--- /dev/null
+++ b/changelogs/unreleased/toggle-sidebar-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Align toggle sidebar button across all browsers and OSs
+merge_request: 22771
+author:
+type: fixed
diff --git a/changelogs/unreleased/update_license_management_job.yml b/changelogs/unreleased/update_license_management_job.yml
new file mode 100644
index 00000000000..d6e56080e77
--- /dev/null
+++ b/changelogs/unreleased/update_license_management_job.yml
@@ -0,0 +1,5 @@
+---
+title: "Remove dind from license_management auto-devops job definition"
+merge_request: 22732
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-bump-gitaly-0-128.yml b/changelogs/unreleased/zj-bump-gitaly-0-128.yml
new file mode 100644
index 00000000000..451df4b800e
--- /dev/null
+++ b/changelogs/unreleased/zj-bump-gitaly-0-128.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.128.0
+merge_request:
+author:
+type: added
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 23790b84e3c..494ddd72556 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -45,4 +45,6 @@ Rails.application.configure do
# Do not log asset requests
config.assets.quiet = true
+
+ config.allow_concurrency = defined?(::Puma)
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 9941987929c..71195164e7a 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -83,5 +83,5 @@ Rails.application.configure do
config.eager_load = true
- config.allow_concurrency = false
+ config.allow_concurrency = defined?(::Puma)
end
diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb
new file mode 100644
index 00000000000..0f45cf44621
--- /dev/null
+++ b/config/initializers/fill_shards.rb
@@ -0,0 +1,4 @@
+return unless Shard.connected?
+return if Gitlab::Database.read_only?
+
+Shard.populate!
diff --git a/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb
new file mode 100644
index 00000000000..61d32fe16eb
--- /dev/null
+++ b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexToProjectDeployTokensDeployTokenId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL already has index inserted
+ add_concurrent_index :project_deploy_tokens, :deploy_token_id if Gitlab::Database.postgresql?
+ end
+
+ def down
+ remove_concurrent_index(:project_deploy_tokens, :deploy_token_id) if Gitlab::Database.postgresql?
+ end
+end
diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb
new file mode 100644
index 00000000000..5e0a6960548
--- /dev/null
+++ b/db/migrate/20181019032400_add_shards_table.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddShardsTable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :shards do |t|
+ t.string :name, null: false, index: { unique: true }
+ end
+ end
+end
diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb
new file mode 100644
index 00000000000..077f264d3ce
--- /dev/null
+++ b/db/migrate/20181019032408_add_repositories_table.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddRepositoriesTable < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :repositories, id: :bigserial do |t|
+ t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
+ t.string :disk_path, null: false, index: { unique: true }
+ end
+
+ add_column :projects, :pool_repository_id, :bigint
+ add_index :projects, :pool_repository_id, where: 'pool_repository_id IS NOT NULL'
+ end
+end
diff --git a/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb
new file mode 100644
index 00000000000..059988de38a
--- /dev/null
+++ b/db/migrate/20181019105553_add_projects_pool_repository_id_foreign_key.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddProjectsPoolRepositoryIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(
+ :projects,
+ :repositories,
+ column: :pool_repository_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:projects, column: :pool_repository_id)
+ end
+end
diff --git a/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb
new file mode 100644
index 00000000000..744748b3fad
--- /dev/null
+++ b/db/migrate/20181023104858_add_archive_builds_duration_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1a8b556228d..32d10e87e87 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false
+ t.integer "archive_builds_in_seconds"
end
create_table "audit_events", force: :cascade do |t|
@@ -1588,6 +1589,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.datetime_with_timezone "created_at", null: false
end
+ add_index "project_deploy_tokens", ["deploy_token_id"], name: "index_project_deploy_tokens_on_deploy_token_id", using: :btree
add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
@@ -1703,6 +1705,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "jobs_cache_index"
t.boolean "pages_https_only", default: true
t.boolean "remote_mirror_available_overridden"
+ t.integer "pool_repository_id", limit: 8
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1719,6 +1722,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
+ add_index "projects", ["pool_repository_id"], name: "index_projects_on_pool_repository_id", where: "(pool_repository_id IS NOT NULL)", using: :btree
add_index "projects", ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree
add_index "projects", ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
@@ -1851,6 +1855,14 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
+ create_table "repositories", id: :bigserial, force: :cascade do |t|
+ t.integer "shard_id", null: false
+ t.string "disk_path", null: false
+ end
+
+ add_index "repositories", ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree
+ add_index "repositories", ["shard_id"], name: "index_repositories_on_shard_id", using: :btree
+
create_table "repository_languages", id: false, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "programming_language_id", null: false
@@ -1931,6 +1943,12 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree
+ create_table "shards", force: :cascade do |t|
+ t.string "name", null: false
+ end
+
+ add_index "shards", ["name"], name: "index_shards_on_name", unique: true, using: :btree
+
create_table "site_statistics", force: :cascade do |t|
t.integer "repositories_count", default: 0, null: false
end
@@ -2450,6 +2468,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
add_foreign_key "project_statistics", "projects", on_delete: :cascade
+ add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade
add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade
@@ -2461,6 +2480,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
+ add_foreign_key "repositories", "shards", on_delete: :restrict
add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 338368dbbc9..b76f9618fc9 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -31,7 +31,7 @@ Database: el_duderino
```
If you are wondering why we used `mysql` for the `Host`, read more at
-[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
+[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-mysql].
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 3fe79943fdc..96f3861f8d7 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -23,6 +23,9 @@ one of the [Merge request coaches][team].
Depending on the areas your merge request touches, it must be **approved** by one
or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer):
+For approvals, we use the approval functionality found in the merge request
+widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval).
+
1. If your merge request includes backend changes [^1], it must be
**approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**.
1. If your merge request includes frontend changes [^1], it must be
@@ -97,6 +100,9 @@ If a developer who happens to also be a maintainer was involved in a merge reque
as a domain expert and/or reviewer, it is recommended that they are not also picked
as the maintainer to ultimately approve and merge it.
+Maintainers should check before merging if the merge request is approved by the
+required approvers.
+
## Best practices
### Everyone
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 1764e2d8b21..5b32b5cd46f 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -171,6 +171,7 @@ the feature you contribute through all of these steps.
1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant
1. Community questions answered
1. Answers to questions radiated (in docs/wiki/support etc.)
+1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions
If you add a dependency in GitLab (such as an operating system package) please
consider updating the following and note the applicability of each in your
@@ -185,7 +186,7 @@ merge request:
1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[testing]: ../testing_guide/index.md
+[testing]: ../testing_guide/index.md
---
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 4359592905d..1fd230a41aa 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -95,6 +95,7 @@ The following table depicts the various user permission levels in a project.
| Manage GitLab Pages | | | | āœ“ | āœ“ |
| Manage GitLab Pages domains and certificates | | | | āœ“ | āœ“ |
| Remove GitLab Pages | | | | | āœ“ |
+| View GitLab Pages protected by [access control](../administration/pages/index.md#access-control) | āœ“ | āœ“ | āœ“ | āœ“ | āœ“ |
| Manage clusters | | | | āœ“ | āœ“ |
| Manage license policy **[ULTIMATE]** | | | | āœ“ | āœ“ |
| Edit comments (posted by any user) | | | | āœ“ | āœ“ |
@@ -206,7 +207,7 @@ They will, like usual users, receive a role in the project or group with all
the abilities that are mentioned in the table above. They cannot however create
groups or projects, and they have the same access as logged out users in all
other cases.
-
+
An administrator can flag a user as external [through the API](../api/users.md)
or by checking the checkbox on the admin panel. As an administrator, navigate
to **Admin > Users** to create a new user or edit an existing one. There, you
@@ -217,7 +218,7 @@ by an administrator under **Admin > Application Settings**.
### Default internal users
-The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users.
+The "Internal users" field allows specifying an e-mail address regex pattern to identify default internal users.
New users whose email address matches the regex pattern will be set to internal by default rather than an external collaborator.
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 47382b09207..2a56506f3a5 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -512,11 +512,9 @@ module API
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_impersonation_token
- finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
+ finder.find_by_id(declared_params[:impersonation_token_id]) || not_found!('Impersonation Token')
end
- # rubocop: enable CodeReuse/ActiveRecord
end
before { authenticated_as_admin! }
diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb
index 6c9125647ad..45d9ba41e92 100644
--- a/lib/gitlab/ci/status/build/action.rb
+++ b/lib/gitlab/ci/status/build/action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 024047d4983..43fb5cdbbe6 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb
index c83e2734a73..0518b9e673d 100644
--- a/lib/gitlab/ci/status/build/canceled.rb
+++ b/lib/gitlab/ci/status/build/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
index c1fc70ac266..6a75ec5c37f 100644
--- a/lib/gitlab/ci/status/build/common.rb
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb
index 5be8e9de425..780fea23123 100644
--- a/lib/gitlab/ci/status/build/created.rb
+++ b/lib/gitlab/ci/status/build/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb
index 495227c2ffb..d74cfc1ee77 100644
--- a/lib/gitlab/ci/status/build/erased.rb
+++ b/lib/gitlab/ci/status/build/erased.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 4a74d6d6ed1..6e4bfe23f2b 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 4babc23a495..d40454df737 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
@@ -12,7 +14,8 @@ module Gitlab
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner',
stale_schedule: 'stale schedule',
- job_execution_timeout: 'job execution timeout'
+ job_execution_timeout: 'job execution timeout',
+ archived_failure: 'archived failure'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index ca0046fb1f7..d7570fdd3e2 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb
index 042da6392d3..d01b09f1398 100644
--- a/lib/gitlab/ci/status/build/manual.rb
+++ b/lib/gitlab/ci/status/build/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb
index 9dd9a27ad57..95f668295dd 100644
--- a/lib/gitlab/ci/status/build/pending.rb
+++ b/lib/gitlab/ci/status/build/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index a8b9ebf0803..c66b8ca5654 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retried.rb b/lib/gitlab/ci/status/build/retried.rb
index 6e190e4ee3c..b489dc68733 100644
--- a/lib/gitlab/ci/status/build/retried.rb
+++ b/lib/gitlab/ci/status/build/retried.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 5aeb8e51480..eb6b3f21604 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb
index 62ad9083616..f443dbee120 100644
--- a/lib/gitlab/ci/status/build/scheduled.rb
+++ b/lib/gitlab/ci/status/build/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb
index 3e678d0baee..4fe2f7b3114 100644
--- a/lib/gitlab/ci/status/build/skipped.rb
+++ b/lib/gitlab/ci/status/build/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index dea838bfa39..a620e7ad126 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb
index e1b7b83428c..9110839cb55 100644
--- a/lib/gitlab/ci/status/build/unschedule.rb
+++ b/lib/gitlab/ci/status/build/unschedule.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e6195a60d4f..07f37732023 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 9d6a2f51c11..ea773ee9944 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 846f00b83dd..fface4bb97b 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 1e8101f8949..b72a28ed0b6 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
index 9307545b5b1..4169f5b3210 100644
--- a/lib/gitlab/ci/status/external/common.rb
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
index 07b15bd8d97..91fafb940a8 100644
--- a/lib/gitlab/ci/status/external/factory.rb
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index 15836c699c7..3446644eff8 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 27ce85bd3ed..770ed7d4d5a 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/common.rb b/lib/gitlab/ci/status/group/common.rb
index cfd4329a923..0b5ea0712ca 100644
--- a/lib/gitlab/ci/status/group/common.rb
+++ b/lib/gitlab/ci/status/group/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/factory.rb b/lib/gitlab/ci/status/group/factory.rb
index d118116cfc3..ee785856fdd 100644
--- a/lib/gitlab/ci/status/group/factory.rb
+++ b/lib/gitlab/ci/status/group/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index fc387e2fd25..50c92add400 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 6780780db32..cea7e6ed938 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
index bf7e484ee9b..ed13a439be0 100644
--- a/lib/gitlab/ci/status/pipeline/blocked.rb
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 61bb07beb0f..7b34a2ea858 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/delayed.rb b/lib/gitlab/ci/status/pipeline/delayed.rb
index 12736861c89..e61acdcd167 100644
--- a/lib/gitlab/ci/status/pipeline/delayed.rb
+++ b/lib/gitlab/ci/status/pipeline/delayed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 0adf83fa197..5d1a8bbd924 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index ee13905e46d..ac7dd74cdce 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index 3adcfa36af2..16ad1da89e3 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0dbdc4de426..aaec1e1d201 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index f60a7662075..f12daaa9676 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 4c37f084d07..58f4642510b 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index 731013ec017..020f2c5b89f 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 32b4cf43e48..6632cd9b143 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 734af5eba59..c759bb7098e 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -49,7 +49,7 @@ variables:
POSTGRES_ENABLED: "true"
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
- KUBERNETES_VERSION: 1.8.6
+ KUBERNETES_VERSION: 1.10.9
HELM_VERSION: 2.11.0
DOCKER_DRIVER: overlay2
@@ -116,12 +116,9 @@ code_quality:
license_management:
stage: test
- image: docker:stable
+ image: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
allow_failure: true
- services:
- - docker:stable-dind
script:
- - setup_docker
- license_management
artifacts:
paths: [gl-license-management-report.json]
@@ -525,11 +522,7 @@ rollout 100%:
}
function license_management() {
- # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable"
- LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
-
- docker run --volume "$PWD:/code" \
- "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code
+ /run.sh analyze .
}
function sast() {
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
index 2147f62a84a..e9b3199d56e 100644
--- a/lib/gitlab/ci/trace/chunked_io.rb
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
# source: https://gitlab.com/snippets/1685610
@@ -66,8 +68,8 @@ module Gitlab
end
end
- def read(length = nil, outbuf = "")
- out = ""
+ def read(length = nil, outbuf = nil)
+ out = []
length ||= size - tell
@@ -83,17 +85,18 @@ module Gitlab
length -= chunk_data.bytesize
end
+ out = out.join
+
# If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
if outbuf
- outbuf.slice!(0, outbuf.bytesize)
- outbuf << out
+ outbuf.replace(out)
end
out
end
def readline
- out = ""
+ out = []
until eof?
data = chunk_slice_from_offset
@@ -109,7 +112,7 @@ module Gitlab
end
end
- out
+ out.join
end
def write(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
index c09089d6475..f33f8cc56c1 100644
--- a/lib/gitlab/ci/trace/section_parser.rb
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index a71040e5e56..bd40fdf59b1 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
@@ -129,8 +131,7 @@ module Gitlab
debris = ''
until (buf = read_backward(BUFFER_SIZE)).empty?
- buf += debris
- debris, *lines = buf.each_line.to_a
+ debris, *lines = (buf + debris).each_line.to_a
lines.reverse_each do |line|
yield(line.force_encoding(Encoding.default_external))
end
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index ad30b3f427c..a7b4e0348c2 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 7da6d09d440..fdf852e8788 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2bed470514b..9790818ecaf 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -92,6 +92,7 @@ excluded_attributes:
- :path
- :namespace_id
- :creator_id
+ - :pool_repository_id
- :import_url
- :import_status
- :avatar
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 4a1bdf34c3e..1cd4f9e17b7 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -2,6 +2,7 @@ module Gitlab
module Kubernetes
module Helm
HELM_VERSION = '2.7.2'.freeze
+ KUBECTL_VERSION = '1.11.0'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
SERVICE_ACCOUNT = 'tiller'.freeze
CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 6752f2cff43..008cba9d33c 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -11,12 +11,6 @@ module Gitlab
def generate_script
<<~HEREDOC
set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
HEREDOC
end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index 95192b11c0d..e9c621d96f0 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -25,7 +25,7 @@ module Gitlab
def container_specification
{
name: 'helm',
- image: 'alpine:3.6',
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/#{Gitlab::Kubernetes::Helm::HELM_VERSION}-kube-#{Gitlab::Kubernetes::Helm::KUBECTL_VERSION}",
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 324e5315821..2f4b0e900c3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2172,7 +2172,7 @@ msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
-msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes."
+msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes."
msgstr ""
msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes."
@@ -4165,6 +4165,9 @@ msgstr ""
msgid "Notes|Show comments only"
msgstr ""
+msgid "Notes|Show history only"
+msgstr ""
+
msgid "Notification events"
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index f00331dfe93..c0d5244dbfa 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -36,42 +36,40 @@ module QA
##
# GitLab QA fabrication mechanisms
#
- module Factory
- autoload :ApiFabricator, 'qa/factory/api_fabricator'
- autoload :Base, 'qa/factory/base'
-
- module Resource
- autoload :Sandbox, 'qa/factory/resource/sandbox'
- autoload :Group, 'qa/factory/resource/group'
- autoload :Issue, 'qa/factory/resource/issue'
- autoload :Project, 'qa/factory/resource/project'
- autoload :Label, 'qa/factory/resource/label'
- autoload :MergeRequest, 'qa/factory/resource/merge_request'
- autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
- autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork'
- autoload :DeployKey, 'qa/factory/resource/deploy_key'
- autoload :DeployToken, 'qa/factory/resource/deploy_token'
- autoload :Branch, 'qa/factory/resource/branch'
- autoload :CiVariable, 'qa/factory/resource/ci_variable'
- autoload :Runner, 'qa/factory/resource/runner'
- autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
- autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
- autoload :User, 'qa/factory/resource/user'
- autoload :ProjectMilestone, 'qa/factory/resource/project_milestone'
- autoload :Wiki, 'qa/factory/resource/wiki'
- autoload :File, 'qa/factory/resource/file'
- autoload :Fork, 'qa/factory/resource/fork'
- autoload :SSHKey, 'qa/factory/resource/ssh_key'
- end
+ module Resource
+ autoload :ApiFabricator, 'qa/resource/api_fabricator'
+ autoload :Base, 'qa/resource/base'
+
+ autoload :Sandbox, 'qa/resource/sandbox'
+ autoload :Group, 'qa/resource/group'
+ autoload :Issue, 'qa/resource/issue'
+ autoload :Project, 'qa/resource/project'
+ autoload :Label, 'qa/resource/label'
+ autoload :MergeRequest, 'qa/resource/merge_request'
+ autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github'
+ autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork'
+ autoload :DeployKey, 'qa/resource/deploy_key'
+ autoload :DeployToken, 'qa/resource/deploy_token'
+ autoload :Branch, 'qa/resource/branch'
+ autoload :CiVariable, 'qa/resource/ci_variable'
+ autoload :Runner, 'qa/resource/runner'
+ autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
+ autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
+ autoload :User, 'qa/resource/user'
+ autoload :ProjectMilestone, 'qa/resource/project_milestone'
+ autoload :Wiki, 'qa/resource/wiki'
+ autoload :File, 'qa/resource/file'
+ autoload :Fork, 'qa/resource/fork'
+ autoload :SSHKey, 'qa/resource/ssh_key'
module Repository
- autoload :Push, 'qa/factory/repository/push'
- autoload :ProjectPush, 'qa/factory/repository/project_push'
- autoload :WikiPush, 'qa/factory/repository/wiki_push'
+ autoload :Push, 'qa/resource/repository/push'
+ autoload :ProjectPush, 'qa/resource/repository/project_push'
+ autoload :WikiPush, 'qa/resource/repository/wiki_push'
end
module Settings
- autoload :HashedStorage, 'qa/factory/settings/hashed_storage'
+ autoload :HashedStorage, 'qa/resource/settings/hashed_storage'
end
end
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
deleted file mode 100644
index 42077f60611..00000000000
--- a/qa/qa/factory/README.md
+++ /dev/null
@@ -1,410 +0,0 @@
-# Factory objects in GitLab QA
-
-In GitLab QA we are using factories to create resources.
-
-Factories implementation are primarily done using Browser UI steps, but can also
-be done via the API.
-
-## Why do we need that?
-
-We need factory objects because we need to reduce duplication when creating
-resources for our QA tests.
-
-## How to properly implement a factory object?
-
-All factories should inherit from [`Factory::Base`](./base.rb).
-
-There is only one mandatory method to implement to define a factory. This is the
-`#fabricate!` method, which is used to build a resource via the browser UI.
-Note that you should only use [Page objects](../page/README.md) to interact with
-a Web page in this method.
-
-Here is an imaginary example:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- def fabricate!
- Page::Dashboard::Index.perform do |dashboard_index|
- dashboard_index.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
- end
- end
- end
- end
-end
-```
-
-### Define API implementation
-
-A factory may also implement the three following methods to be able to create a
-resource via the public GitLab API:
-
-- `#api_get_path`: The `GET` path to fetch an existing resource.
-- `#api_post_path`: The `POST` path to create a new resource.
-- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
-
-Let's take the `Shirt` factory example, and add these three API methods:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- def fabricate!
- # ... same as before
- end
-
- def api_get_path
- "/shirt/#{name}"
- end
-
- def api_post_path
- "/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
- end
- end
- end
-end
-```
-
-The [`Project` factory](./resource/project.rb) is a good real example of Browser
-UI and API implementations.
-
-#### Resource attributes
-
-A resource may need another resource to exist first. For instance, a project
-needs a group to be created in.
-
-To define a resource attribute, you can use the `attribute` method with a
-block using the other factory to fabricate the resource.
-
-That will allow access to the other resource from your resource object's
-methods. You would usually use it in `#fabricate!`, `#api_get_path`,
-`#api_post_path`, `#api_post_body`.
-
-Let's take the `Shirt` factory, and add a `project` attribute to it:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
- end
-
- def api_get_path
- "/project/#{project.path}/shirt/#{name}"
- end
-
- def api_post_path
- "/project/#{project.path}/shirts"
- end
-
- def api_post_body
- {
- name: name
- }
- end
- end
- end
- end
-end
-```
-
-**Note that all the attributes are lazily constructed. This means if you want
-a specific attribute to be fabricated first, you'll need to call the
-attribute method first even if you're not using it.**
-
-#### Product data attributes
-
-Once created, you may want to populate a resource with attributes that can be
-found in the Web page, or in the API response.
-For instance, once you create a project, you may want to store its repository
-SSH URL as an attribute.
-
-Again we could use the `attribute` method with a block, using a page object
-to retrieve the data on the page.
-
-Let's take the `Shirt` factory, and define a `:brand` attribute:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- # Attribute populated from the Browser UI (using the block)
- attribute :brand do
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-**Note again that all the attributes are lazily constructed. This means if
-you call `shirt.brand` after moving to the other page, it'll not properly
-retrieve the data because we're no longer on the expected page.**
-
-Consider this:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.project.visit!
-
-shirt.brand # => FAIL!
-```
-
-The above example will fail because now we're on the project page, trying to
-construct the brand data from the shirt page, however we moved to the project
-page already. There are two ways to solve this, one is that we could try to
-retrieve the brand before visiting the project again:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.brand # => OK!
-
-shirt.project.visit!
-
-shirt.brand # => OK!
-```
-
-The attribute will be stored in the instance therefore all the following calls
-will be fine, using the data previously constructed. If we think that this
-might be too brittle, we could eagerly construct the data right before
-ending fabrication:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- # ... same as before
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform do |project_show|
- project_show.go_to_new_shirt
- end
-
- Page::Shirt::New.perform do |shirt_new|
- shirt_new.set_name(name)
- shirt_new.create_shirt!
- end
-
- populate(:brand) # Eagerly construct the data
- end
- end
- end
- end
-end
-```
-
-The `populate` method will iterate through its arguments and call each
-attribute respectively. Here `populate(:brand)` has the same effect as
-just `brand`. Using the populate method makes the intention clearer.
-
-With this, it will make sure we construct the data right after we create the
-shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data.
-
-Alternatively, we could just make sure we're on the right page before
-constructing the brand data:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attr_accessor :name
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-create-a-shirt'
- end
- end
-
- # Attribute populated from the Browser UI (using the block)
- attribute :brand do
- back_url = current_url
- visit!
-
- Page::Shirt::Show.perform do |shirt_show|
- shirt_show.fetch_brand_from_page
- end
-
- visit(back_url)
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-This will make sure it's on the shirt page before constructing brand, and
-move back to the previous page to avoid breaking the state.
-
-#### Define an attribute based on an API response
-
-Sometimes, you want to define a resource attribute based on the API response
-from its `GET` or `POST` request. For instance, if the creation of a shirt via
-the API returns
-
-```ruby
-{
- brand: 'a-brand-new-brand',
- style: 't-shirt',
- materials: [[:cotton, 80], [:polyamide, 20]]
-}
-```
-
-you may want to store `style` as-is in the resource, and fetch the first value
-of the first `materials` item in a `main_fabric` attribute.
-
-Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
-attributes:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- # ... same as before
-
- # Attribute from the Shirt factory if present,
- # or fetched from the API response if present,
- # or a QA::Factory::Base::NoValueError is raised otherwise
- attribute :style
-
- # If the attribute from the Shirt factory is not present,
- # and if the API does not contain this field, this block will be
- # used to construct the value based on the API response.
- attribute :main_fabric do
- api_response.&dig(:materials, 0, 0)
- end
-
- # ... same as before
- end
- end
- end
-end
-```
-
-**Notes on attributes precedence:**
-
-- factory instance variables have the highest precedence
-- attributes from the API response take precedence over attributes from the
- block (usually from Browser UI)
-- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
-
-## Creating resources in your tests
-
-To create a resource in your tests, you can call the `.fabricate!` method on the
-factory class.
-Note that if the factory supports API fabrication, this will use this
-fabrication by default.
-
-Here is an example that will use the API fabrication method under the hood since
-it's supported by the `Shirt` factory:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
- shirt.name = 'my-shirt'
-end
-
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
-expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
-expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
-expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
-```
-
-If you explicitly want to use the Browser UI fabrication method, you can call
-the `.fabricate_via_browser_ui!` method instead:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
- shirt.name = 'my-shirt'
-end
-
-expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
-expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
-expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
-expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
-```
-
-You can also explicitly use the API fabrication method, by calling the
-`.fabricate_via_api!` method:
-
-```ruby
-my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
- shirt.name = 'my-shirt'
-end
-```
-
-In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
-
-## Where to ask for help?
-
-If you need more information, ask for help on `#quality` channel on Slack
-(internal, GitLab Team only).
-
-If you are not a Team Member, and you still need help to contribute, please
-open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
deleted file mode 100644
index b05d1e252ec..00000000000
--- a/qa/qa/factory/resource/branch.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-module QA
- module Factory
- module Resource
- class Branch < Factory::Base
- attr_accessor :project, :branch_name,
- :allow_to_push, :allow_to_merge, :protected
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'protected-branch-project'
- end
- end
-
- def initialize
- @branch_name = 'test/branch'
- @allow_to_push = true
- @allow_to_merge = true
- @protected = false
- end
-
- def fabricate!
- project.visit!
-
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'kick-off.txt'
- resource.commit_message = 'First commit'
- end
-
- branch = Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'README.md'
- resource.commit_message = 'Add readme'
- resource.branch_name = 'master'
- resource.new_branch = false
- resource.remote_branch = @branch_name
- end
-
- Page::Project::Show.perform do |page|
- page.wait { page.has_content?(branch_name) }
- end
-
- # The upcoming process will make it access the Protected Branches page,
- # select the already created branch and protect it according
- # to `allow_to_push` variable.
- return branch unless @protected
-
- Page::Project::Menu.perform(&:click_repository_settings)
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_protected_branches do |page|
- page.select_branch(branch_name)
-
- if allow_to_push
- page.allow_devs_and_maintainers_to_push
- else
- page.allow_no_one_to_push
- end
-
- if allow_to_merge
- page.allow_devs_and_maintainers_to_merge
- else
- page.allow_no_one_to_merge
- end
-
- page.wait(reload: false) do
- !page.first('.btn-success').disabled?
- end
-
- page.protect_branch
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb
deleted file mode 100644
index a0aefc61f9f..00000000000
--- a/qa/qa/factory/resource/ci_variable.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class CiVariable < Factory::Base
- attr_accessor :key, :value
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-ci-variables'
- resource.description = 'project for adding CI variable test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_ci_cd_settings)
-
- Page::Project::Settings::CICD.perform do |setting|
- setting.expand_ci_variables do |page|
- page.fill_variable(key, value)
-
- page.save_variables
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
deleted file mode 100644
index aea99c9f80d..00000000000
--- a/qa/qa/factory/resource/deploy_key.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployKey < Factory::Base
- attr_accessor :title, :key
-
- attribute :fingerprint do
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |key|
- key_offset = key.key_titles.index do |key_title|
- key_title.text == title
- end
-
- key.key_fingerprints[key_offset].text
- end
- end
- end
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-deploy'
- resource.description = 'project for adding deploy key test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_repository_settings)
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_keys do |page|
- page.fill_key_title(title)
- page.fill_key_value(key)
-
- page.add_key
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb
deleted file mode 100644
index 68e98f0aa01..00000000000
--- a/qa/qa/factory/resource/deploy_token.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module QA
- module Factory
- module Resource
- class DeployToken < Factory::Base
- attr_accessor :name, :expires_at
-
- attribute :username do
- Page::Project::Settings::Repository.perform do |page|
- page.expand_deploy_tokens do |token|
- token.token_username
- end
- end
- end
-
- attribute :password do
- Page::Project::Settings::Repository.perform do |page|
- page.expand_deploy_tokens do |token|
- token.token_password
- end
- end
- end
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-to-deploy'
- resource.description = 'project for adding deploy token test'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.act do
- click_repository_settings
- end
-
- Page::Project::Settings::Repository.perform do |setting|
- setting.expand_deploy_tokens do |page|
- page.fill_token_name(name)
- page.fill_token_expires_at(expires_at)
- page.fill_scopes(read_repository: true, read_registry: false)
-
- page.add_token
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb
deleted file mode 100644
index 1148876c2d3..00000000000
--- a/qa/qa/factory/resource/file.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class File < Factory::Base
- attr_accessor :name,
- :content,
- :commit_message
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-new-file'
- end
- end
-
- def initialize
- @name = 'QA Test - File name'
- @content = 'QA Test - File content'
- @commit_message = 'QA Test - Commit message'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform(&:create_new_file!)
-
- Page::File::Form.perform do |page|
- page.add_name(@name)
- page.add_content(@content)
- page.add_commit_message(@commit_message)
- page.commit_changes
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
deleted file mode 100644
index b1e874af893..00000000000
--- a/qa/qa/factory/resource/fork.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-module QA
- module Factory
- module Resource
- class Fork < Factory::Base
- attribute :push do
- Factory::Repository::ProjectPush.fabricate!
- end
-
- attribute :user do
- Factory::Resource::User.fabricate! do |resource|
- if Runtime::Env.forker?
- resource.username = Runtime::Env.forker_username
- resource.password = Runtime::Env.forker_password
- end
- end
- end
-
- def visit_project_with_retry
- # The user intermittently fails to stay signed in after visiting the
- # project page. The new user is registered and then signs in and a
- # screenshot shows that signing in was successful. Then the project
- # page is visited but a screenshot shows the user is no longer signed
- # in. It's difficult to reproduce locally but GDK logs don't seem to
- # show anything unexpected. This method attempts to work around the
- # problem and capture data to help troubleshoot.
-
- Capybara::Screenshot.screenshot_and_save_page
-
- start = Time.now
-
- while Time.now - start < 20
- push.project.visit!
-
- puts "Visited project page"
- Capybara::Screenshot.screenshot_and_save_page
-
- return if Page::Main::Menu.act { has_personal_area?(wait: 0) }
-
- puts "Not signed in. Attempting to sign in again."
- Capybara::Screenshot.screenshot_and_save_page
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
-
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(user)
- end
- end
-
- raise "Failed to load project page and stay logged in"
- end
-
- def fabricate!
- populate(:push, :user)
-
- visit_project_with_retry
-
- Page::Project::Show.perform(&:fork_project)
-
- Page::Project::Fork::New.perform do |fork_new|
- fork_new.choose_namespace(user.name)
- end
-
- Page::Layout::Banner.perform do |page|
- page.has_notice?('The project was successfully forked.')
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
deleted file mode 100644
index 45e49da86f9..00000000000
--- a/qa/qa/factory/resource/group.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-module QA
- module Factory
- module Resource
- class Group < Factory::Base
- attr_accessor :path, :description
-
- attribute :sandbox do
- Factory::Resource::Sandbox.fabricate!
- end
-
- attribute :id
-
- def initialize
- @path = Runtime::Namespace.name
- @description = "QA test run at #{Runtime::Namespace.time}"
- end
-
- def fabricate!
- sandbox.visit!
-
- Page::Group::Show.perform do |group_show|
- if group_show.has_subgroup?(path)
- group_show.go_to_subgroup(path)
- else
- group_show.go_to_new_subgroup
-
- Page::Group::New.perform do |group_new|
- group_new.set_path(path)
- group_new.set_description(description)
- group_new.set_visibility('Public')
- group_new.create
- end
-
- # Ensure that the group was actually created
- group_show.wait(time: 1) do
- group_show.has_text?(path) &&
- group_show.has_new_project_or_subgroup_dropdown?
- end
- end
- end
- end
-
- def fabricate_via_api!
- resource_web_url(api_get)
- rescue ResourceNotFoundError
- super
- end
-
- def api_get_path
- "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
- end
-
- def api_post_path
- '/groups'
- end
-
- def api_post_body
- {
- parent_id: sandbox.id,
- path: path,
- name: path,
- visibility: 'public'
- }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
deleted file mode 100644
index 3a28e0d5aa6..00000000000
--- a/qa/qa/factory/resource/issue.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class Issue < Factory::Base
- attr_writer :description
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-for-issues'
- resource.description = 'project for adding issues'
- end
- end
-
- attribute :title
-
- def fabricate!
- project.visit!
-
- Page::Project::Show.perform(&:go_to_new_issue)
-
- Page::Project::Issue::New.perform do |page|
- page.add_title(@title)
- page.add_description(@description)
- page.create_new_issue
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
deleted file mode 100644
index aac6864f42f..00000000000
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class KubernetesCluster < Factory::Base
- attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
-
- attribute :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
- end
-
- def fabricate!
- @project.visit!
-
- Page::Project::Menu.perform(
- &:click_operations_kubernetes)
-
- Page::Project::Operations::Kubernetes::Index.perform(
- &:add_kubernetes_cluster)
-
- Page::Project::Operations::Kubernetes::Add.perform(
- &:add_existing_cluster)
-
- Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
- page.set_cluster_name(@cluster.cluster_name)
- page.set_api_url(@cluster.api_url)
- page.set_ca_certificate(@cluster.ca_certificate)
- page.set_token(@cluster.token)
- page.check_rbac! if @cluster.rbac
- page.add_cluster!
- end
-
- if @install_helm_tiller
- Page::Project::Operations::Kubernetes::Show.perform do |page|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 10
-
- # Helm must be installed before everything else
- page.install!(:helm)
- page.await_installed(:helm)
-
- page.install!(:ingress) if @install_ingress
- page.install!(:prometheus) if @install_prometheus
- page.install!(:runner) if @install_runner
-
- page.await_installed(:ingress) if @install_ingress
- page.await_installed(:prometheus) if @install_prometheus
- page.await_installed(:runner) if @install_runner
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb
deleted file mode 100644
index 32bc519b48c..00000000000
--- a/qa/qa/factory/resource/label.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Label < Factory::Base
- attr_accessor :description, :color
-
- attribute :title
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-label'
- end
- end
-
- def initialize
- @title = "qa-test-#{SecureRandom.hex(8)}"
- @description = 'This is a test label'
- @color = '#0033CC'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:go_to_labels)
- Page::Label::Index.perform(&:go_to_new_label)
-
- Page::Label::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.fill_color(@color)
- page.create_label
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
deleted file mode 100644
index 4b7d2287f98..00000000000
--- a/qa/qa/factory/resource/merge_request.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class MergeRequest < Factory::Base
- attr_accessor :title,
- :description,
- :source_branch,
- :target_branch,
- :assignee,
- :milestone,
- :labels
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-merge-request'
- end
- end
-
- attribute :target do
- project.visit!
-
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.branch_name = 'master'
- resource.remote_branch = target_branch
- end
- end
-
- attribute :source do
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.branch_name = target_branch
- resource.remote_branch = source_branch
- resource.new_branch = false
- resource.file_name = "added_file.txt"
- resource.file_content = "File Added"
- end
- end
-
- def initialize
- @title = 'QA test - merge request'
- @description = 'This is a test merge request'
- @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
- @target_branch = "master"
- @assignee = nil
- @milestone = nil
- @labels = []
- end
-
- def fabricate!
- populate(:target, :source)
-
- project.visit!
- Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.choose_milestone(@milestone) if @milestone
- labels.each do |label|
- page.select_label(label)
- end
-
- page.create_merge_request
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
deleted file mode 100644
index 1311bf625a6..00000000000
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module QA
- module Factory
- module Resource
- class MergeRequestFromFork < MergeRequest
- attr_accessor :fork_branch
-
- attribute :fork do
- Factory::Resource::Fork.fabricate!
- end
-
- attribute :push do
- Factory::Repository::ProjectPush.fabricate! do |resource|
- resource.project = fork
- resource.branch_name = fork_branch
- resource.file_name = 'file2.txt'
- resource.user = fork.user
- end
- end
-
- def fabricate!
- populate(:push)
-
- fork.visit!
-
- Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform(&:create_merge_request)
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb
deleted file mode 100644
index ceb0f1c3d75..00000000000
--- a/qa/qa/factory/resource/personal_access_token.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Create a personal access token that can be used by the api
- #
- class PersonalAccessToken < Factory::Base
- attr_accessor :name
-
- attribute :access_token do
- Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_profile_settings)
- Page::Profile::Menu.perform(&:click_access_tokens)
-
- Page::Profile::PersonalAccessTokens.perform do |page|
- page.fill_token_name(name || 'api-test-token')
- page.check_api
- page.create_token
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
deleted file mode 100644
index f691ae5a342..00000000000
--- a/qa/qa/factory/resource/project.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Project < Factory::Base
- attribute :name
- attribute :description
-
- attribute :group do
- Factory::Resource::Group.fabricate!
- end
-
- attribute :repository_ssh_location do
- Page::Project::Show.perform do |page|
- page.choose_repository_clone_ssh
- page.repository_location
- end
- end
-
- attribute :repository_http_location do
- Page::Project::Show.perform do |page|
- page.choose_repository_clone_http
- page.repository_location
- end
- end
-
- def initialize
- @description = 'My awesome project'
- end
-
- def name=(raw_name)
- @name = "#{raw_name}-#{SecureRandom.hex(8)}"
- end
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.perform(&:go_to_new_project)
-
- Page::Project::New.perform do |page|
- page.choose_test_namespace
- page.choose_name(@name)
- page.add_description(@description)
- page.set_visibility('Public')
- page.create_new_project
- end
- end
-
- def api_get_path
- "/projects/#{name}"
- end
-
- def api_post_path
- '/projects'
- end
-
- def api_post_body
- {
- namespace_id: group.id,
- path: name,
- name: name,
- description: description,
- visibility: 'public'
- }
- end
-
- private
-
- def transform_api_resource(resource)
- resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo])
- resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo])
- resource
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
deleted file mode 100644
index ce20641e6cc..00000000000
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class ProjectImportedFromGithub < Resource::Project
- attr_accessor :name
- attr_writer :personal_access_token, :github_repository_path
-
- attribute :group do
- Factory::Resource::Group.fabricate!
- end
-
- def fabricate!
- group.visit!
-
- Page::Group::Show.perform(&:go_to_new_project)
-
- Page::Project::New.perform do |page|
- page.go_to_import_project
- end
-
- Page::Project::New.perform do |page|
- page.go_to_github_import
- end
-
- Page::Project::Import::Github.perform do |page|
- page.add_personal_access_token(@personal_access_token)
- page.list_repos
- page.import!(@github_repository_path, @name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
deleted file mode 100644
index 383f534c12c..00000000000
--- a/qa/qa/factory/resource/project_milestone.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module QA
- module Factory
- module Resource
- class ProjectMilestone < Factory::Base
- attr_reader :title
- attr_accessor :description
-
- attribute :project do
- Factory::Resource::Project.fabricate!
- end
-
- def title=(title)
- @title = "#{title}-#{SecureRandom.hex(4)}"
- @description = 'A milestone'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform do |page|
- page.click_issues
- page.click_milestones
- end
-
- Page::Project::Milestone::Index.perform(&:click_new_milestone)
-
- Page::Project::Milestone::New.perform do |milestone_new|
- milestone_new.set_title(@title)
- milestone_new.set_description(@description)
- milestone_new.create_new_milestone
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb
deleted file mode 100644
index 7108db1e55a..00000000000
--- a/qa/qa/factory/resource/runner.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class Runner < Factory::Base
- attr_writer :name, :tags, :image
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-ci-cd'
- resource.description = 'Project with CI/CD Pipelines'
- end
- end
-
- def name
- @name || "qa-runner-#{SecureRandom.hex(4)}"
- end
-
- def tags
- @tags || %w[qa e2e]
- end
-
- def image
- @image || 'gitlab/gitlab-runner:alpine'
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform(&:click_ci_cd_settings)
-
- Service::Runner.new(name).tap do |runner|
- Page::Project::Settings::CICD.perform do |settings|
- settings.expand_runners_settings do |runners|
- runner.pull
- runner.token = runners.registration_token
- runner.address = runners.coordinator_address
- runner.tags = tags
- runner.image = image
- runner.register!
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
deleted file mode 100644
index a125bac65dd..00000000000
--- a/qa/qa/factory/resource/sandbox.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module QA
- module Factory
- module Resource
- ##
- # Ensure we're in our sandbox namespace, either by navigating to it or by
- # creating it if it doesn't yet exist.
- #
- class Sandbox < Factory::Base
- attr_reader :path
-
- attribute :id
-
- def initialize
- @path = Runtime::Namespace.sandbox_name
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_groups)
-
- Page::Dashboard::Groups.perform do |page|
- if page.has_group?(path)
- page.go_to_group(path)
- else
- page.go_to_new_group
-
- Page::Group::New.perform do |group|
- group.set_path(path)
- group.set_description('GitLab QA Sandbox Group')
- group.set_visibility('Public')
- group.create
- end
- end
- end
- end
-
- def fabricate_via_api!
- resource_web_url(api_get)
- rescue ResourceNotFoundError
- super
- end
-
- def api_get_path
- "/groups/#{path}"
- end
-
- def api_post_path
- '/groups'
- end
-
- def api_post_body
- {
- path: path,
- name: path,
- visibility: 'public'
- }
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
deleted file mode 100644
index 6f952eda36f..00000000000
--- a/qa/qa/factory/resource/ssh_key.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Factory
- module Resource
- class SSHKey < Factory::Base
- extend Forwardable
-
- attr_accessor :title
-
- def_delegators :key, :private_key, :public_key, :fingerprint
-
- def key
- @key ||= Runtime::Key::RSA.new
- end
-
- def fabricate!
- Page::Main::Menu.perform(&:go_to_profile_settings)
- Page::Profile::Menu.perform(&:click_ssh_keys)
-
- Page::Profile::SSHKeys.perform do |page|
- page.add_key(public_key, title)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
deleted file mode 100644
index e361face1f0..00000000000
--- a/qa/qa/factory/resource/user.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'securerandom'
-
-module QA
- module Factory
- module Resource
- class User < Factory::Base
- attr_reader :unique_id
- attr_writer :username, :password
-
- def initialize
- @unique_id = SecureRandom.hex(8)
- end
-
- def username
- @username ||= "qa-user-#{unique_id}"
- end
-
- def password
- @password ||= 'password'
- end
-
- def name
- @name ||= username
- end
-
- def email
- @email ||= "#{username}@example.com"
- end
-
- def credentials_given?
- defined?(@username) && defined?(@password)
- end
-
- def fabricate!
- # Don't try to log-out if we're not logged-in
- if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
- Page::Main::Menu.perform { |main| main.sign_out }
- end
-
- if credentials_given?
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(self)
- end
- else
- Page::Main::Login.perform do |login|
- login.switch_to_register_tab
- end
- Page::Main::SignUp.perform do |signup|
- signup.sign_up!(self)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
deleted file mode 100644
index 769f394e85c..00000000000
--- a/qa/qa/factory/resource/wiki.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module QA
- module Factory
- module Resource
- class Wiki < Factory::Base
- attr_accessor :title, :content, :message
-
- attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-for-wikis'
- resource.description = 'project for adding wikis'
- end
- end
-
- def fabricate!
- project.visit!
-
- Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
-
- Page::Project::Wiki::New.perform do |wiki_new|
- wiki_new.go_to_create_first_page
- wiki_new.set_title(@title)
- wiki_new.set_content(@content)
- wiki_new.set_message(@message)
- wiki_new.create_new_page
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 94b9486b0d5..97ffe0e5716 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -65,7 +65,7 @@ module QA
end
def sign_in_using_admin_credentials
- admin = QA::Factory::Resource::User.new.tap do |user|
+ admin = QA::Resource::User.new.tap do |user|
user.username = QA::Runtime::User.admin_username
user.password = QA::Runtime::User.admin_password
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 376606afb5d..2e69a89e386 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module QA
module Page
module MergeRequest
@@ -23,6 +25,32 @@ module QA
element :squash_checkbox
end
+ view 'app/views/projects/merge_requests/show.html.haml' do
+ element :notes_tab
+ element :diffs_tab
+ end
+
+ view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do
+ element :diff_comment
+ end
+
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :note_dropdown
+ element :discussion_option
+ end
+
+ view 'app/assets/javascripts/notes/components/note_form.vue' do
+ element :reply_input
+ end
+
+ view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do
+ element :discussion_reply
+ end
+
+ view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do
+ element :new_diff_line
+ end
+
view 'app/views/shared/issuable/_sidebar.html.haml' do
element :labels_block
end
@@ -106,6 +134,35 @@ module QA
click_element :squash_checkbox
end
+
+ def go_to_discussions_tab
+ click_element :notes_tab
+ end
+
+ def go_to_diffs_tab
+ click_element :diffs_tab
+ end
+
+ def add_comment_to_diff(text)
+ wait(time: 5) do
+ page.has_text?("No newline at end of file")
+ end
+ all_elements(:new_diff_line).first.hover
+ click_element :diff_comment
+ fill_element :reply_input, text
+ end
+
+ def start_discussion(text)
+ fill_element :comment_input, text
+ click_element :note_dropdown
+ click_element :discussion_option
+ click_element :comment_button
+ end
+
+ def reply_to_discussion(reply_text)
+ all_elements(:discussion_reply).last.click
+ fill_element :reply_input, reply_text
+ end
end
end
end
diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md
new file mode 100644
index 00000000000..4cdeb3f42a2
--- /dev/null
+++ b/qa/qa/resource/README.md
@@ -0,0 +1,392 @@
+# Resource class in GitLab QA
+
+Resources are primarily created using Browser UI steps, but can also
+be created via the API.
+
+## How to properly implement a resource class?
+
+All resource classes should inherit from [`Resource::Base`](./base.rb).
+
+There is only one mandatory method to implement to define a resource class.
+This is the `#fabricate!` method, which is used to build the resource via the
+browser UI. Note that you should only use [Page objects](../page/README.md) to
+interact with a Web page in this method.
+
+Here is an imaginary example:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ Page::Dashboard::Index.perform do |dashboard_index|
+ dashboard_index.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+ end
+ end
+end
+```
+
+### Define API implementation
+
+A resource class may also implement the three following methods to be able to
+create the resource via the public GitLab API:
+
+- `#api_get_path`: The `GET` path to fetch an existing resource.
+- `#api_post_path`: The `POST` path to create a new resource.
+- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
+
+Let's take the `Shirt` resource class, and add these three API methods:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ def fabricate!
+ # ... same as before
+ end
+
+ def api_get_path
+ "/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+The [`Project` resource](./project.rb) is a good real example of Browser
+UI and API implementations.
+
+#### Resource attributes
+
+A resource may need another resource to exist first. For instance, a project
+needs a group to be created in.
+
+To define a resource attribute, you can use the `attribute` method with a
+block using the other resource class to fabricate the resource.
+
+That will allow access to the other resource from your resource object's
+methods. You would usually use it in `#fabricate!`, `#api_get_path`,
+`#api_post_path`, `#api_post_body`.
+
+Let's take the `Shirt` resource class, and add a `project` attribute to it:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
+```
+
+**Note that all the attributes are lazily constructed. This means if you want
+a specific attribute to be fabricated first, you'll need to call the
+attribute method first even if you're not using it.**
+
+#### Product data attributes
+
+Once created, you may want to populate a resource with attributes that can be
+found in the Web page, or in the API response.
+For instance, once you create a project, you may want to store its repository
+SSH URL as an attribute.
+
+Again we could use the `attribute` method with a block, using a page object
+to retrieve the data on the page.
+
+Let's take the `Shirt` resource class, and define a `:brand` attribute:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Note again that all the attributes are lazily constructed. This means if
+you call `shirt.brand` after moving to the other page, it'll not properly
+retrieve the data because we're no longer on the expected page.**
+
+Consider this:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.project.visit!
+
+shirt.brand # => FAIL!
+```
+
+The above example will fail because now we're on the project page, trying to
+construct the brand data from the shirt page, however we moved to the project
+page already. There are two ways to solve this, one is that we could try to
+retrieve the brand before visiting the project again:
+
+```ruby
+shirt =
+ QA::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.brand # => OK!
+
+shirt.project.visit!
+
+shirt.brand # => OK!
+```
+
+The attribute will be stored in the instance therefore all the following calls
+will be fine, using the data previously constructed. If we think that this
+might be too brittle, we could eagerly construct the data right before
+ending fabrication:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+
+ populate(:brand) # Eagerly construct the data
+ end
+ end
+ end
+end
+```
+
+The `populate` method will iterate through its arguments and call each
+attribute respectively. Here `populate(:brand)` has the same effect as
+just `brand`. Using the populate method makes the intention clearer.
+
+With this, it will make sure we construct the data right after we create the
+shirt. The drawback is that this will always construct the data when the
+resource is fabricated even if we don't need to use the data.
+
+Alternatively, we could just make sure we're on the right page before
+constructing the brand data:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ attr_accessor :name
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ back_url = current_url
+ visit!
+
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+
+ visit(back_url)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+This will make sure it's on the shirt page before constructing brand, and
+move back to the previous page to avoid breaking the state.
+
+#### Define an attribute based on an API response
+
+Sometimes, you want to define a resource attribute based on the API response
+from its `GET` or `POST` request. For instance, if the creation of a shirt via
+the API returns
+
+```ruby
+{
+ brand: 'a-brand-new-brand',
+ style: 't-shirt',
+ materials: [[:cotton, 80], [:polyamide, 20]]
+}
+```
+
+you may want to store `style` as-is in the resource, and fetch the first value
+of the first `materials` item in a `main_fabric` attribute.
+
+Let's take the `Shirt` resource class, and define a `:style` and a
+`:main_fabric` attributes:
+
+```ruby
+module QA
+ module Resource
+ class Shirt < Base
+ # ... same as before
+
+ # @style from the instance if present,
+ # or fetched from the API response if present,
+ # or a QA::Resource::Base::NoValueError is raised otherwise
+ attribute :style
+
+ # If @main_fabric is not present,
+ # and if the API does not contain this field, this block will be
+ # used to construct the value based on the API response, and
+ # store the result in @main_fabric
+ attribute :main_fabric do
+ api_response.&dig(:materials, 0, 0)
+ end
+
+ # ... same as before
+ end
+ end
+end
+```
+
+**Notes on attributes precedence:**
+
+- resource instance variables have the highest precedence
+- attributes from the API response take precedence over attributes from the
+ block (usually from Browser UI)
+- attributes without a value will raise a `QA::Resource::Base::NoValueError` error
+
+## Creating resources in your tests
+
+To create a resource in your tests, you can call the `.fabricate!` method on
+the resource class.
+Note that if the resource class supports API fabrication, this will use this
+fabrication by default.
+
+Here is an example that will use the API fabrication method under the hood
+since it's supported by the `Shirt` resource class:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
+expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
+```
+
+If you explicitly want to use the Browser UI fabrication method, you can call
+the `.fabricate_via_browser_ui!` method instead:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
+expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
+expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided
+expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
+```
+
+You can also explicitly use the API fabrication method, by calling the
+`.fabricate_via_api!` method:
+
+```ruby
+my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
+ shirt.name = 'my-shirt'
+end
+```
+
+In this case, the result will be similar to calling
+`Resource::Shirt.fabricate!`.
+
+## Where to ask for help?
+
+If you need more information, ask for help on `#quality` channel on Slack
+(internal, GitLab Team only).
+
+If you are not a Team Member, and you still need help to contribute, please
+open an issue in GitLab CE issue tracker with the `~QA` label.
diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index b1cfb6c9783..3762a94f312 100644
--- a/qa/qa/factory/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -5,7 +5,7 @@ require 'active_support/core_ext/object/deep_dup'
require 'capybara/dsl'
module QA
- module Factory
+ module Resource
module ApiFabricator
include Airborne
include Capybara::DSL
@@ -27,7 +27,7 @@ module QA
def fabricate_via_api!
unless api_support?
- raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!"
+ raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
end
resource_web_url(api_post)
@@ -52,14 +52,18 @@ module QA
end
def api_get
- url = Runtime::API::Request.new(api_client, api_get_path).url
+ process_api_response(parse_body(api_get_from(api_get_path)))
+ end
+
+ def api_get_from(get_path)
+ url = Runtime::API::Request.new(api_client, get_path).url
response = get(url)
unless response.code == HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
end
- process_api_response(parse_body(response))
+ response
end
def api_post
@@ -89,8 +93,8 @@ module QA
self.api_resource = transform_api_resource(parsed_response.deep_dup)
end
- def transform_api_resource(resource)
- resource
+ def transform_api_resource(api_resource)
+ api_resource
end
end
end
diff --git a/qa/qa/factory/base.rb b/qa/qa/resource/base.rb
index 75438b77bf3..f3eefb70520 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/resource/base.rb
@@ -4,7 +4,7 @@ require 'forwardable'
require 'capybara/dsl'
module QA
- module Factory
+ module Resource
class Base
extend SingleForwardable
include ApiFabricator
@@ -58,11 +58,11 @@ module QA
def self.fabricate_via_browser_ui!(*args, &prepare_block)
options = args.extract_options!
- factory = options.fetch(:factory) { new }
+ resource = options.fetch(:resource) { new }
parents = options.fetch(:parents) { [] }
- do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
current_url
end
@@ -70,29 +70,29 @@ module QA
def self.fabricate_via_api!(*args, &prepare_block)
options = args.extract_options!
- factory = options.fetch(:factory) { new }
+ resource = options.fetch(:resource) { new }
parents = options.fetch(:parents) { [] }
- raise NotImplementedError unless factory.api_support?
+ raise NotImplementedError unless resource.api_support?
- factory.eager_load_api_client!
+ resource.eager_load_api_client!
- do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! }
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
end
end
- def self.do_fabricate!(factory:, prepare_block:, parents: [])
- prepare_block.call(factory) if prepare_block
+ def self.do_fabricate!(resource:, prepare_block:, parents: [])
+ prepare_block.call(resource) if prepare_block
resource_web_url = yield
- factory.web_url = resource_web_url
+ resource.web_url = resource_web_url
- factory
+ resource
end
private_class_method :do_fabricate!
- def self.log_fabrication(method, factory, parents, args)
+ def self.log_fabrication(method, resource, parents, args)
return yield unless Runtime::Env.debug?
start = Time.now
@@ -111,7 +111,7 @@ module QA
private_class_method :log_fabrication
def self.evaluator
- @evaluator ||= Factory::Base::DSL.new(self)
+ @evaluator ||= Base::DSL.new(self)
end
private_class_method :evaluator
diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb
new file mode 100644
index 00000000000..bd52c4abe02
--- /dev/null
+++ b/qa/qa/resource/branch.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Branch < Base
+ attr_accessor :project, :branch_name,
+ :allow_to_push, :allow_to_merge, :protected
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'protected-branch-project'
+ end
+ end
+
+ def initialize
+ @branch_name = 'test/branch'
+ @allow_to_push = true
+ @allow_to_merge = true
+ @protected = false
+ end
+
+ def fabricate!
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'kick-off.txt'
+ resource.commit_message = 'First commit'
+ end
+
+ branch = Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.file_name = 'README.md'
+ resource.commit_message = 'Add readme'
+ resource.branch_name = 'master'
+ resource.new_branch = false
+ resource.remote_branch = @branch_name
+ end
+
+ Page::Project::Show.perform do |page|
+ page.wait { page.has_content?(branch_name) }
+ end
+
+ # The upcoming process will make it access the Protected Branches page,
+ # select the already created branch and protect it according
+ # to `allow_to_push` variable.
+ return branch unless @protected
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_protected_branches do |page|
+ page.select_branch(branch_name)
+
+ if allow_to_push
+ page.allow_devs_and_maintainers_to_push
+ else
+ page.allow_no_one_to_push
+ end
+
+ if allow_to_merge
+ page.allow_devs_and_maintainers_to_merge
+ else
+ page.allow_no_one_to_merge
+ end
+
+ page.wait(reload: false) do
+ !page.first('.btn-success').disabled?
+ end
+
+ page.protect_branch
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb
new file mode 100644
index 00000000000..0570c47d41c
--- /dev/null
+++ b/qa/qa/resource/ci_variable.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class CiVariable < Base
+ attr_accessor :key, :value
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-variables'
+ resource.description = 'project for adding CI variable test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Page::Project::Settings::CICD.perform do |setting|
+ setting.expand_ci_variables do |page|
+ page.fill_variable(key, value)
+
+ page.save_variables
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
new file mode 100644
index 00000000000..9ed8fb7726e
--- /dev/null
+++ b/qa/qa/resource/deploy_key.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployKey < Base
+ attr_accessor :title, :key
+
+ attribute :fingerprint do
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |key_title|
+ key_title.text == title
+ end
+
+ key.key_fingerprints[key_offset].text
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy key test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_repository_settings)
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_keys do |page|
+ page.fill_key_title(title)
+ page.fill_key_value(key)
+
+ page.add_key
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb
new file mode 100644
index 00000000000..cee4422f6b4
--- /dev/null
+++ b/qa/qa/resource/deploy_token.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class DeployToken < Base
+ attr_accessor :name, :expires_at
+
+ attribute :username do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_username
+ end
+ end
+ end
+
+ attribute :password do
+ Page::Project::Settings::Repository.perform do |page|
+ page.expand_deploy_tokens do |token|
+ token.token_password
+ end
+ end
+ end
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-to-deploy'
+ resource.description = 'project for adding deploy token test'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.act do
+ click_repository_settings
+ end
+
+ Page::Project::Settings::Repository.perform do |setting|
+ setting.expand_deploy_tokens do |page|
+ page.fill_token_name(name)
+ page.fill_token_expires_at(expires_at)
+ page.fill_scopes(read_repository: true, read_registry: false)
+
+ page.add_token
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
new file mode 100644
index 00000000000..effc5a7940b
--- /dev/null
+++ b/qa/qa/resource/file.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class File < Base
+ attr_accessor :name,
+ :content,
+ :commit_message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-new-file'
+ end
+ end
+
+ def initialize
+ @name = 'QA Test - File name'
+ @content = 'QA Test - File content'
+ @commit_message = 'QA Test - Commit message'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:create_new_file!)
+
+ Page::File::Form.perform do |page|
+ page.add_name(@name)
+ page.add_content(@content)
+ page.add_commit_message(@commit_message)
+ page.commit_changes
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
new file mode 100644
index 00000000000..9fd66f3a36a
--- /dev/null
+++ b/qa/qa/resource/fork.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Fork < Base
+ attribute :push do
+ Repository::ProjectPush.fabricate!
+ end
+
+ attribute :user do
+ User.fabricate! do |resource|
+ if Runtime::Env.forker?
+ resource.username = Runtime::Env.forker_username
+ resource.password = Runtime::Env.forker_password
+ end
+ end
+ end
+
+ def fabricate!
+ populate(:push, :user)
+
+ # Sign out as admin and sign is as the fork user
+ Page::Main::Menu.perform(&:sign_out)
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(user)
+ end
+
+ push.project.visit!
+
+ Page::Project::Show.perform(&:fork_project)
+
+ Page::Project::Fork::New.perform do |fork_new|
+ fork_new.choose_namespace(user.name)
+ end
+
+ Page::Layout::Banner.perform do |page|
+ page.has_notice?('The project was successfully forked.')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
new file mode 100644
index 00000000000..dce15e4f10b
--- /dev/null
+++ b/qa/qa/resource/group.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Group < Base
+ attr_accessor :path, :description
+
+ attribute :sandbox do
+ Sandbox.fabricate!
+ end
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def fabricate!
+ sandbox.visit!
+
+ Page::Group::Show.perform do |group_show|
+ if group_show.has_subgroup?(path)
+ group_show.go_to_subgroup(path)
+ else
+ group_show.go_to_new_subgroup
+
+ Page::Group::New.perform do |group_new|
+ group_new.set_path(path)
+ group_new.set_description(description)
+ group_new.set_visibility('Public')
+ group_new.create
+ end
+
+ # Ensure that the group was actually created
+ group_show.wait(time: 1) do
+ group_show.has_text?(path) &&
+ group_show.has_new_project_or_subgroup_dropdown?
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ parent_id: sandbox.id,
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
new file mode 100644
index 00000000000..2c2f27fe231
--- /dev/null
+++ b/qa/qa/resource/issue.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Issue < Base
+ attr_writer :description
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-issues'
+ resource.description = 'project for adding issues'
+ end
+ end
+
+ attribute :title
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform(&:go_to_new_issue)
+
+ Page::Project::Issue::New.perform do |page|
+ page.add_title(@title)
+ page.add_description(@description)
+ page.create_new_issue
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
new file mode 100644
index 00000000000..96c8843fb99
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class KubernetesCluster < Base
+ attr_writer :project, :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
+
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
+ end
+
+ def fabricate!
+ @project.visit!
+
+ Page::Project::Menu.perform(
+ &:click_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
+
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
+ page.set_cluster_name(@cluster.cluster_name)
+ page.set_api_url(@cluster.api_url)
+ page.set_ca_certificate(@cluster.ca_certificate)
+ page.set_token(@cluster.token)
+ page.check_rbac! if @cluster.rbac
+ page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |page|
+ # We must wait a few seconds for permissions to be set up correctly for new cluster
+ sleep 10
+
+ # Helm must be installed before everything else
+ page.install!(:helm)
+ page.await_installed(:helm)
+
+ page.install!(:ingress) if @install_ingress
+ page.install!(:prometheus) if @install_prometheus
+ page.install!(:runner) if @install_runner
+
+ page.await_installed(:ingress) if @install_ingress
+ page.await_installed(:prometheus) if @install_prometheus
+ page.await_installed(:runner) if @install_runner
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb
new file mode 100644
index 00000000000..c0869cb1f2a
--- /dev/null
+++ b/qa/qa/resource/label.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Label < Base
+ attr_accessor :description, :color
+
+ attribute :title
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-label'
+ end
+ end
+
+ def initialize
+ @title = "qa-test-#{SecureRandom.hex(8)}"
+ @description = 'This is a test label'
+ @color = '#0033CC'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_labels)
+ Page::Label::Index.perform(&:go_to_new_label)
+
+ Page::Label::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.fill_color(@color)
+ page.create_label
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
new file mode 100644
index 00000000000..466a7942dc6
--- /dev/null
+++ b/qa/qa/resource/merge_request.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class MergeRequest < Base
+ attr_accessor :title,
+ :description,
+ :source_branch,
+ :target_branch,
+ :assignee,
+ :milestone,
+ :labels
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-merge-request'
+ end
+ end
+
+ attribute :target do
+ project.visit!
+
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = 'master'
+ resource.remote_branch = target_branch
+ end
+ end
+
+ attribute :source do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = project
+ resource.branch_name = target_branch
+ resource.remote_branch = source_branch
+ resource.new_branch = false
+ resource.file_name = "added_file.txt"
+ resource.file_content = "File Added"
+ end
+ end
+
+ def initialize
+ @title = 'QA test - merge request'
+ @description = 'This is a test merge request'
+ @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
+ @target_branch = "master"
+ @assignee = nil
+ @milestone = nil
+ @labels = []
+ end
+
+ def fabricate!
+ populate(:target, :source)
+
+ project.visit!
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.choose_milestone(@milestone) if @milestone
+ labels.each do |label|
+ page.select_label(label)
+ end
+
+ page.create_merge_request
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
new file mode 100644
index 00000000000..f91ae299d76
--- /dev/null
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class MergeRequestFromFork < MergeRequest
+ attr_accessor :fork_branch
+
+ attribute :fork do
+ Fork.fabricate!
+ end
+
+ attribute :push do
+ Repository::ProjectPush.fabricate! do |resource|
+ resource.project = fork
+ resource.branch_name = fork_branch
+ resource.file_name = 'file2.txt'
+ resource.user = fork.user
+ end
+ end
+
+ def fabricate!
+ populate(:push)
+
+ fork.visit!
+
+ Page::Project::Show.perform(&:new_merge_request)
+ Page::MergeRequest::New.perform(&:create_merge_request)
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb
new file mode 100644
index 00000000000..b8dd0a3562f
--- /dev/null
+++ b/qa/qa/resource/personal_access_token.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Create a personal access token that can be used by the api
+ #
+ class PersonalAccessToken < Base
+ attr_accessor :name
+
+ attribute :access_token do
+ Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_access_tokens)
+
+ Page::Profile::PersonalAccessTokens.perform do |page|
+ page.fill_token_name(name || 'api-test-token')
+ page.check_api
+ page.create_token
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
new file mode 100644
index 00000000000..7fdf69278f9
--- /dev/null
+++ b/qa/qa/resource/project.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Project < Base
+ attribute :name
+ attribute :description
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ attribute :repository_ssh_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_ssh
+ page.repository_location
+ end
+ end
+
+ attribute :repository_http_location do
+ Page::Project::Show.perform do |page|
+ page.choose_repository_clone_http
+ page.repository_location
+ end
+ end
+
+ def initialize
+ @description = 'My awesome project'
+ end
+
+ def name=(raw_name)
+ @name = "#{raw_name}-#{SecureRandom.hex(8)}"
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.choose_test_namespace
+ page.choose_name(@name)
+ page.add_description(@description)
+ page.set_visibility('Public')
+ page.create_new_project
+ end
+ end
+
+ def api_get_path
+ "/projects/#{name}"
+ end
+
+ def api_post_path
+ '/projects'
+ end
+
+ def api_post_body
+ {
+ namespace_id: group.id,
+ path: name,
+ name: name,
+ description: description,
+ visibility: 'public'
+ }
+ end
+
+ private
+
+ def transform_api_resource(api_resource)
+ api_resource[:repository_ssh_location] =
+ Git::Location.new(api_resource[:ssh_url_to_repo])
+ api_resource[:repository_http_location] =
+ Git::Location.new(api_resource[:http_url_to_repo])
+ api_resource
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb
new file mode 100644
index 00000000000..3f02fe885a9
--- /dev/null
+++ b/qa/qa/resource/project_imported_from_github.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class ProjectImportedFromGithub < Project
+ attr_accessor :name
+ attr_writer :personal_access_token, :github_repository_path
+
+ attribute :group do
+ Group.fabricate!
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.perform(&:go_to_new_project)
+
+ Page::Project::New.perform do |page|
+ page.go_to_import_project
+ end
+
+ Page::Project::New.perform do |page|
+ page.go_to_github_import
+ end
+
+ Page::Project::Import::Github.perform do |page|
+ page.add_personal_access_token(@personal_access_token)
+ page.list_repos
+ page.import!(@github_repository_path, @name)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb
new file mode 100644
index 00000000000..a4d6657caff
--- /dev/null
+++ b/qa/qa/resource/project_milestone.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class ProjectMilestone < Base
+ attr_reader :title
+ attr_accessor :description
+
+ attribute :project do
+ Project.fabricate!
+ end
+
+ def title=(title)
+ @title = "#{title}-#{SecureRandom.hex(4)}"
+ @description = 'A milestone'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform do |page|
+ page.click_issues
+ page.click_milestones
+ end
+
+ Page::Project::Milestone::Index.perform(&:click_new_milestone)
+
+ Page::Project::Milestone::New.perform do |milestone_new|
+ milestone_new.set_title(@title)
+ milestone_new.set_description(@description)
+ milestone_new.create_new_milestone
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index 272b7fc5818..c9fafe3419f 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class ProjectPush < Factory::Repository::Push
+ class ProjectPush < Repository::Push
attribute :project do
- Factory::Resource::Project.fabricate! do |resource|
+ Project.fabricate! do |resource|
resource.name = 'project-with-code'
resource.description = 'Project with repository'
end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb
index ffa755b9e88..c14d97ff7fb 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/resource/repository/push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'pathname'
module QA
- module Factory
+ module Resource
module Repository
- class Push < Factory::Base
+ class Push < Base
attr_accessor :file_name, :file_content, :commit_message,
:branch_name, :new_branch, :output, :repository_http_uri,
:repository_ssh_uri, :ssh_key, :user
diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb
index 25b6ffe8323..f1c39d507fe 100644
--- a/qa/qa/factory/repository/wiki_push.rb
+++ b/qa/qa/resource/repository/wiki_push.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Repository
- class WikiPush < Factory::Repository::Push
+ class WikiPush < Repository::Push
attribute :wiki do
- Factory::Resource::Wiki.fabricate! do |resource|
+ Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
new file mode 100644
index 00000000000..08ae3f22117
--- /dev/null
+++ b/qa/qa/resource/runner.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class Runner < Base
+ attr_writer :name, :tags, :image
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-with-ci-cd'
+ resource.description = 'Project with CI/CD Pipelines'
+ end
+ end
+
+ def name
+ @name || "qa-runner-#{SecureRandom.hex(4)}"
+ end
+
+ def tags
+ @tags || %w[qa e2e]
+ end
+
+ def image
+ @image || 'gitlab/gitlab-runner:alpine'
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_settings)
+
+ Service::Runner.new(name).tap do |runner|
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_runners_settings do |runners|
+ runner.pull
+ runner.token = runners.registration_token
+ runner.address = runners.coordinator_address
+ runner.tags = tags
+ runner.image = image
+ runner.register!
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
new file mode 100644
index 00000000000..41ce857a8b8
--- /dev/null
+++ b/qa/qa/resource/sandbox.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ ##
+ # Ensure we're in our sandbox namespace, either by navigating to it or by
+ # creating it if it doesn't yet exist.
+ #
+ class Sandbox < Base
+ attr_reader :path
+
+ attribute :id
+
+ def initialize
+ @path = Runtime::Namespace.sandbox_name
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_groups)
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(path)
+ page.go_to_group(path)
+ else
+ page.go_to_new_group
+
+ Page::Group::New.perform do |group|
+ group.set_path(path)
+ group.set_description('GitLab QA Sandbox Group')
+ group.set_visibility('Public')
+ group.create
+ end
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/groups/#{path}"
+ end
+
+ def api_post_path
+ '/groups'
+ end
+
+ def api_post_body
+ {
+ path: path,
+ name: path,
+ visibility: 'public'
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb
index 4e32382f910..40c06768ffe 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/resource/settings/hashed_storage.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module QA
- module Factory
+ module Resource
module Settings
- class HashedStorage < Factory::Base
+ class HashedStorage < Base
def fabricate!(*traits)
raise ArgumentError unless traits.include?(:enabled)
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
new file mode 100644
index 00000000000..c6c97c8532f
--- /dev/null
+++ b/qa/qa/resource/ssh_key.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class SSHKey < Base
+ extend Forwardable
+
+ attr_accessor :title
+
+ def_delegators :key, :private_key, :public_key, :fingerprint
+
+ def key
+ @key ||= Runtime::Key::RSA.new
+ end
+
+ def fabricate!
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+
+ Page::Profile::SSHKeys.perform do |page|
+ page.add_key(public_key, title)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
new file mode 100644
index 00000000000..16f0b311fa9
--- /dev/null
+++ b/qa/qa/resource/user.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ class User < Base
+ attr_reader :unique_id
+ attr_writer :username, :password
+
+ def initialize
+ @unique_id = SecureRandom.hex(8)
+ end
+
+ def username
+ @username ||= "qa-user-#{unique_id}"
+ end
+
+ def password
+ @password ||= 'password'
+ end
+
+ def name
+ @name ||= username
+ end
+
+ def email
+ @email ||= "#{username}@example.com"
+ end
+
+ def credentials_given?
+ defined?(@username) && defined?(@password)
+ end
+
+ def fabricate!
+ # Don't try to log-out if we're not logged-in
+ if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
+ Page::Main::Menu.perform { |main| main.sign_out }
+ end
+
+ if credentials_given?
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(self)
+ end
+ else
+ Page::Main::Login.perform do |login|
+ login.switch_to_register_tab
+ end
+ Page::Main::SignUp.perform do |signup|
+ signup.sign_up!(self)
+ end
+ end
+ end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def api_get_path
+ "/users/#{fetch_id(username)}"
+ end
+
+ def api_post_path
+ '/users'
+ end
+
+ def api_post_body
+ {
+ email: email,
+ password: password,
+ username: username,
+ name: name,
+ skip_confirmation: true
+ }
+ end
+
+ private
+
+ def fetch_id(username)
+ users = parse_body(api_get_from("/users?username=#{username}"))
+
+ unless users.size == 1 && users.first[:username] == username
+ raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`."
+ end
+
+ users.first[:id]
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb
new file mode 100644
index 00000000000..e942e9718a0
--- /dev/null
+++ b/qa/qa/resource/wiki.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Wiki < Base
+ attr_accessor :title, :content, :message
+
+ attribute :project do
+ Project.fabricate! do |resource|
+ resource.name = 'project-for-wikis'
+ resource.description = 'project for adding wikis'
+ end
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
+
+ Page::Project::Wiki::New.perform do |wiki_new|
+ wiki_new.go_to_create_first_page
+ wiki_new.set_title(@title)
+ wiki_new.set_content(@content)
+ wiki_new.set_message(@message)
+ wiki_new.create_new_page
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 0545b500e4c..aff84c89f0e 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -32,7 +32,7 @@ module QA
def do_create_personal_access_token
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ Resource::PersonalAccessToken.fabricate!.access_token
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 44071ec3e45..7d5fc3c4b65 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -5,7 +5,7 @@ module QA
it 'user registers and logs in' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Factory::Resource::User.fabricate!
+ Resource::User.fabricate!
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index 7bf26c22fa6..bef89d5be24 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -5,18 +5,16 @@ module QA
describe 'Add project member' do
it 'user adds project member' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- user = Factory::Resource::User.fabricate!
+ user = Resource::User.fabricate!
- Page::Main::Menu.perform { |main| main.sign_out }
- Page::Main::Login.act { sign_in_using_credentials }
-
- project = Factory::Resource::Project.fabricate! do |resource|
+ project = Resource::Project.fabricate! do |resource|
resource.name = 'add-member-project'
end
project.visit!
- Page::Project::Menu.act { click_members_settings }
+ Page::Project::Menu.perform(&:click_members_settings)
Page::Project::Settings::Members.perform do |page|
page.add_member(user.username)
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index a242f2158da..6632c2977ef 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project|
+ created_project = Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index a99b0522e73..3ce48de2c25 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -4,7 +4,7 @@ module QA
context 'Manage', :orchestrated, :github do
describe 'Project import from GitHub' do
let(:imported_project) do
- Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
+ Resource::ProjectImportedFromGithub.fabricate! do |project|
project.name = 'imported-project'
project.personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = 'gitlab-qa/test-project'
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index 768d40f3acf..275de3d332c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index e67561b3a39..f5002c8032f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate! do |issue|
issue.title = issue_title
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 24877d937d2..83603f1cda7 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate! do |issue|
issue.title = issue_title
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index 037ff5efbd4..d33947f41da 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -7,22 +7,22 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request-and-milestone'
end
- current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone|
+ current_milestone = Resource::ProjectMilestone.fabricate! do |milestone|
milestone.title = 'unique-milestone'
milestone.project = current_project
end
- new_label = Factory::Resource::Label.fabricate! do |label|
+ new_label = Resource::Label.fabricate! do |label|
label.project = current_project
label.title = 'qa-mr-test-label'
label.description = 'Merge Request label'
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request with a milestone'
merge_request.description = 'Great feature with milestone'
merge_request.project = current_project
@@ -49,11 +49,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- current_project = Factory::Resource::Project.fabricate! do |project|
+ current_project = Resource::Project.fabricate! do |project|
project.name = 'project-with-merge-request'
end
- Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ Resource::MergeRequest.fabricate! do |merge_request|
merge_request.title = 'This is a merge request'
merge_request.description = 'Great feature'
merge_request.project = current_project
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 058af8aebdd..6dcd74471fe 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request|
merge_request.fork_branch = 'feature-branch'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 3bcf086d332..e2d639fd150 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
project.visit!
@@ -15,12 +15,12 @@ module QA
Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only }
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Needs rebasing'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = "other.txt"
push.file_content = "New file added!"
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index 724c48cd125..6ff7360c413 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -7,16 +7,16 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = "squash-before-merge"
end
- merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
merge_request.title = 'Squashing commits'
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = 'to be squashed'
push.branch_name = merge_request.source_branch
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index 7705e12b95e..297485dd81e 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -13,7 +13,7 @@ module QA
before(:all) do
login
- @project = Factory::Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Files view'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index df70b9608d9..94be66782c6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -9,7 +9,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index b18dee53cbc..6a0add56fe0 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -14,7 +14,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |scenario|
+ project = Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index f65a1569fb0..46346d1b984 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Factory::Resource::File.fabricate! do |file|
+ Resource::File.fabricate! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 8e4210482a2..a63b7dce8d6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -7,14 +7,14 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ access_token = Resource::PersonalAccessToken.fabricate!.access_token
- user = Factory::Resource::User.new.tap do |user|
+ user = Resource::User.new.tap do |user|
user.username = Runtime::User.username
user.password = access_token
end
- push = Factory::Repository::ProjectPush.fabricate! do |push|
+ push = Resource::Repository::ProjectPush.fabricate! do |push|
push.user = user
push.file_name = 'README.md'
push.file_content = '# This is a test project'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 2f63a07e0c3..92f596a44d9 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
push.commit_message = 'Add README.md'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index ac71cf52b6f..73a3dc14a65 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -6,7 +6,7 @@ module QA
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
- Factory::Resource::Project.fabricate! do |resource|
+ Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
@@ -47,7 +47,7 @@ module QA
end
def create_protected_branch(allow_to_push:)
- Factory::Resource::Branch.fabricate! do |resource|
+ Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = allow_to_push
@@ -56,7 +56,7 @@ module QA
end
def push_new_file(branch)
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.file_name = 'new_file.md'
resource.file_content = '# This is a new file'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
index 36068ffba69..9c764424129 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
@@ -12,11 +12,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Factory::Resource::SSHKey.fabricate! do |resource|
+ key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.ssh_key = key
push.file_name = 'README.md'
push.file_content = '# Test Use SSH Key'
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 07dbf39a8a3..e7374377104 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -13,7 +13,7 @@ module QA
before(:all) do
login
- @project = Factory::Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index 4126fd9fd3e..210271705d9 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -18,7 +18,7 @@ module QA
end
it 'user creates, edits, clones, and pushes to the wiki' do
- wiki = Factory::Resource::Wiki.fabricate! do |resource|
+ wiki = Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
@@ -34,7 +34,7 @@ module QA
validate_content('My Second Wiki Content')
- Factory::Repository::WikiPush.fabricate! do |push|
+ Resource::Repository::WikiPush.fabricate! do |push|
push.wiki = wiki
push.file_name = 'Home.md'
push.file_content = '# My Third Wiki Content'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 58b272adcf1..0837b720df1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -7,7 +7,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
resource.value = 'some CI variable'
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index d66bcce879b..25cbe41c684 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -13,18 +13,18 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |project|
+ project = Resource::Project.fabricate! do |project|
project.name = 'project-with-pipelines'
project.description = 'Project with CI/CD Pipelines.'
end
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = %w[qa test]
end
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
index 5d9aa00582f..3af7db751e7 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
@@ -13,7 +13,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Runner.fabricate! do |runner|
+ Resource::Runner.fabricate! do |runner|
runner.name = executor
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 64b98da8bf5..84757f25379 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -11,7 +11,7 @@ module QA
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
- deploy_key = Factory::Resource::DeployKey.fabricate! do |resource|
+ deploy_key = Resource::DeployKey.fabricate! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index 604641e54b8..e2320c92343 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -15,13 +15,13 @@ module QA
@runner_name = "qa-runner-#{Time.now.to_i}"
- @project = Factory::Resource::Project.fabricate! do |resource|
+ @project = Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
@repository_location = @project.repository_ssh_location
- Factory::Resource::Runner.fabricate! do |resource|
+ Resource::Runner.fabricate! do |resource|
resource.project = @project
resource.name = @runner_name
resource.tags = %w[qa docker]
@@ -47,7 +47,7 @@ module QA
login
- Factory::Resource::DeployKey.fabricate! do |resource|
+ Resource::DeployKey.fabricate! do |resource|
resource.project = @project
resource.title = "deploy key #{key.name}(#{key.bits})"
resource.key = key.public_key
@@ -55,7 +55,7 @@ module QA
deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = deploy_key_name
resource.value = key.private_key
@@ -78,7 +78,7 @@ module QA
- docker
YAML
- Factory::Repository::ProjectPush.fabricate! do |resource|
+ Resource::Repository::ProjectPush.fabricate! do |resource|
resource.project = @project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
index 263ba6a6800..9f34e4218c1 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
@@ -10,7 +10,7 @@ module QA
deploy_token_name = 'deploy token name'
deploy_token_expires_at = Date.today + 7 # 1 Week from now
- deploy_token = Factory::Resource::DeployToken.fabricate! do |resource|
+ deploy_token = Resource::DeployToken.fabricate! do |resource|
resource.name = deploy_token_name
resource.expires_at = deploy_token_expires_at
end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index c2fce1e7df1..30ec0665973 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -15,21 +15,21 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- project = Factory::Resource::Project.fabricate! do |p|
+ project = Resource::Project.fabricate! do |p|
p.name = 'project-with-autodevops'
p.description = 'Project with Auto Devops'
end
# Disable code_quality check in Auto DevOps pipeline as it takes
# too long and times out the test
- Factory::Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate! do |resource|
resource.project = project
resource.key = 'CODE_QUALITY_DISABLED'
resource.value = '1'
end
# Create Auto Devops compatible repo
- Factory::Repository::ProjectPush.fabricate! do |push|
+ Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.directory = Pathname
.new(__dir__)
@@ -41,7 +41,7 @@ module QA
# Create and connect K8s cluster
@cluster = Service::KubernetesCluster.new(rbac: rbac).create!
- kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster|
+ kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster|
cluster.project = project
cluster.cluster = @cluster
cluster.install_helm_tiller = true
diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb
new file mode 100644
index 00000000000..820c506b715
--- /dev/null
+++ b/qa/spec/factory/resource/user_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+describe QA::Resource::User do
+ describe "#fabricate_via_api!" do
+ Response = Struct.new(:code, :body)
+
+ it 'fetches an existing user' do
+ existing_users = [
+ {
+ id: '0',
+ name: 'name',
+ username: 'name',
+ web_url: ''
+ }
+ ]
+ users_response = Response.new('200', JSON.dump(existing_users))
+ single_user_response = Response.new('200', JSON.dump(existing_users.first))
+
+ expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response)
+ expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response)
+
+ subject.username = 'name'
+ subject.fabricate_via_api!
+
+ expect(subject.api_response).to eq(existing_users.first)
+ end
+
+ it 'tries to create a user if it does not exist' do
+ expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]'))
+ expect(subject).to receive(:api_post).and_return({ web_url: '' })
+
+ subject.username = 'foo'
+ subject.fabricate_via_api!
+ end
+ end
+end
diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb
index e5fbc064911..a5ed4422f6e 100644
--- a/qa/spec/factory/api_fabricator_spec.rb
+++ b/qa/spec/resource/api_fabricator_spec.rb
@@ -1,18 +1,18 @@
# frozen_string_literal: true
-describe QA::Factory::ApiFabricator do
- let(:factory_without_api_support) do
+describe QA::Resource::ApiFabricator do
+ let(:resource_without_api_support) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
end
end
- let(:factory_with_api_support) do
+ let(:resource_with_api_support) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
def api_get_path
@@ -33,22 +33,22 @@ describe QA::Factory::ApiFabricator do
allow(subject).to receive(:current_url).and_return('')
end
- subject { factory.tap { |f| f.include(described_class) }.new }
+ subject { resource.tap { |f| f.include(described_class) }.new }
describe '#api_support?' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
- context 'when factory does not support fabrication via the API' do
- let(:factory) { factory_without_api_support }
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
it 'returns false' do
expect(subject).not_to be_api_support
end
end
- context 'when factory supports fabrication via the API' do
- let(:factory) { factory_with_api_support }
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
it 'returns false' do
expect(subject).to be_api_support
@@ -67,20 +67,20 @@ describe QA::Factory::ApiFabricator do
allow(api_client_instance).to receive(:personal_access_token).and_return('foo')
end
- context 'when factory does not support fabrication via the API' do
- let(:factory) { factory_without_api_support }
+ context 'when resource does not support fabrication via the API' do
+ let(:resource) { resource_without_api_support }
it 'raises a NotImplementedError exception' do
- expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!")
+ expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!")
end
end
- context 'when factory supports fabrication via the API' do
- let(:factory) { factory_with_api_support }
+ context 'when resource supports fabrication via the API' do
+ let(:resource) { resource_with_api_support }
let(:api_request) { spy('Runtime::API::Request') }
let(:resource_web_url) { 'http://example.org/api/v4/foo' }
- let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
- let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) }
+ let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
+ let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) }
before do
stub_const('QA::Runtime::API::Request', api_request)
@@ -103,7 +103,7 @@ describe QA::Factory::ApiFabricator do
it 'populates api_resource with the resource' do
subject.fabricate_via_api!
- expect(subject.api_resource).to eq(resource)
+ expect(subject.api_resource).to eq(response)
end
context 'when the POST fails' do
@@ -114,17 +114,17 @@ describe QA::Factory::ApiFabricator do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
- expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.")
+ expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.")
expect(subject.api_resource).to be_nil
end
end
end
context '#transform_api_resource' do
- let(:factory) do
+ let(:resource) do
Class.new do
def self.name
- 'FooBarFactory'
+ 'FooBarResource'
end
def api_get_path
@@ -146,12 +146,12 @@ describe QA::Factory::ApiFabricator do
end
end
- let(:resource) { { existing: 'foo', web_url: resource_web_url } }
+ let(:response) { { existing: 'foo', web_url: resource_web_url } }
let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } }
it 'transforms the resource' do
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
- expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource)
+ expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource)
subject.fabricate_via_api!
end
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/resource/base_spec.rb
index 990eba76460..dc9e16792d3 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/resource/base_spec.rb
@@ -1,49 +1,49 @@
# frozen_string_literal: true
-describe QA::Factory::Base do
+describe QA::Resource::Base do
include Support::StubENV
- let(:factory) { spy('factory') }
+ let(:resource) { spy('resource') }
let(:location) { 'http://location' }
shared_context 'fabrication context' do
subject do
Class.new(described_class) do
def self.name
- 'MyFactory'
+ 'MyResource'
end
end
end
before do
allow(subject).to receive(:current_url).and_return(location)
- allow(subject).to receive(:new).and_return(factory)
+ allow(subject).to receive(:new).and_return(resource)
end
end
shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
- it 'yields factory before calling factory method' do
- expect(factory).to receive(:something!).ordered
- expect(factory).to receive(fabrication_method_used).ordered.and_return(location)
+ it 'yields resource before calling resource method' do
+ expect(resource).to receive(:something!).ordered
+ expect(resource).to receive(fabrication_method_used).ordered.and_return(location)
- subject.public_send(fabrication_method_called, factory: factory) do |factory|
- factory.something!
+ subject.public_send(fabrication_method_called, resource: resource) do |resource|
+ resource.something!
end
end
- it 'does not log the factory and build method when QA_DEBUG=false' do
+ it 'does not log the resource and build method when QA_DEBUG=false' do
stub_env('QA_DEBUG', 'false')
- expect(factory).to receive(fabrication_method_used).and_return(location)
+ expect(resource).to receive(fabrication_method_used).and_return(location)
- expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
+ expect { subject.public_send(fabrication_method_called, 'something', resource: resource) }
.not_to output.to_stdout
end
end
describe '.fabricate!' do
- context 'when factory does not support fabrication via the API' do
+ context 'when resource does not support fabrication via the API' do
before do
expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
end
@@ -55,7 +55,7 @@ describe QA::Factory::Base do
end
end
- context 'when factory supports fabrication via the API' do
+ context 'when resource supports fabrication via the API' do
it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_api!)
@@ -69,20 +69,20 @@ describe QA::Factory::Base do
it_behaves_like 'fabrication method', :fabricate_via_api!
- it 'instantiates the factory, calls factory method returns the resource' do
- expect(factory).to receive(:fabricate_via_api!).and_return(location)
+ it 'instantiates the resource, calls resource method returns the resource' do
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
- result = subject.fabricate_via_api!(factory: factory, parents: [])
+ result = subject.fabricate_via_api!(resource: resource, parents: [])
- expect(result).to eq(factory)
+ expect(result).to eq(resource)
end
- it 'logs the factory and build method when QA_DEBUG=true' do
+ it 'logs the resource and build method when QA_DEBUG=true' do
stub_env('QA_DEBUG', 'true')
- expect(factory).to receive(:fabricate_via_api!).and_return(location)
+ expect(resource).to receive(:fabricate_via_api!).and_return(location)
- expect { subject.fabricate_via_api!('something', factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via api in [\d\.\-e]+ seconds+/)
+ expect { subject.fabricate_via_api!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/)
.to_stdout
end
end
@@ -92,30 +92,30 @@ describe QA::Factory::Base do
it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
- it 'instantiates the factory and calls factory method' do
- subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
+ it 'instantiates the resource and calls resource method' do
+ subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
- expect(factory).to have_received(:fabricate!).with('something')
+ expect(resource).to have_received(:fabricate!).with('something')
end
it 'returns fabrication resource' do
- result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
+ result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: [])
- expect(result).to eq(factory)
+ expect(result).to eq(resource)
end
- it 'logs the factory and build method when QA_DEBUG=true' do
+ it 'logs the resource and build method when QA_DEBUG=true' do
stub_env('QA_DEBUG', 'true')
- expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via browser_ui in [\d\.\-e]+ seconds+/)
+ expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) }
+ .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/)
.to_stdout
end
end
- shared_context 'simple factory' do
+ shared_context 'simple resource' do
subject do
- Class.new(QA::Factory::Base) do
+ Class.new(QA::Resource::Base) do
attribute :test do
'block'
end
@@ -132,11 +132,11 @@ describe QA::Factory::Base do
end
end
- let(:factory) { subject.new }
+ let(:resource) { subject.new }
end
describe '.attribute' do
- include_context 'simple factory'
+ include_context 'simple resource'
it 'appends new attribute' do
expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
@@ -144,7 +144,7 @@ describe QA::Factory::Base do
context 'when the attribute is populated via a block' do
it 'returns value from the block' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.test).to eq('block')
@@ -155,11 +155,11 @@ describe QA::Factory::Base do
let(:api_resource) { { no_block: 'api' } }
before do
- expect(factory).to receive(:api_resource).and_return(api_resource)
+ expect(resource).to receive(:api_resource).and_return(api_resource)
end
it 'returns value from api' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.no_block).to eq('api')
@@ -173,7 +173,7 @@ describe QA::Factory::Base do
end
it 'returns value from api and emits an INFO log entry' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.test).to eq('api_with_block')
@@ -185,11 +185,11 @@ describe QA::Factory::Base do
context 'when the attribute is populated via direct assignment' do
before do
- factory.test = 'value'
+ resource.test = 'value'
end
it 'returns value from the assignment' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.test).to eq('value')
@@ -197,11 +197,11 @@ describe QA::Factory::Base do
context 'when the api also has such response' do
before do
- allow(factory).to receive(:api_resource).and_return({ test: 'api' })
+ allow(resource).to receive(:api_resource).and_return({ test: 'api' })
end
it 'returns value from the assignment' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect(result).to be_a(described_class)
expect(result.test).to eq('value')
@@ -211,36 +211,36 @@ describe QA::Factory::Base do
context 'when the attribute has no value' do
it 'raises an error because no values could be found' do
- result = subject.fabricate!(factory: factory)
+ result = subject.fabricate!(resource: resource)
expect { result.no_block }
- .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.")
+ .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.")
end
end
end
describe '#web_url' do
- include_context 'simple factory'
+ include_context 'simple resource'
it 'sets #web_url to #current_url after fabrication' do
- subject.fabricate!(factory: factory)
+ subject.fabricate!(resource: resource)
- expect(factory.web_url).to eq(subject.current_url)
+ expect(resource.web_url).to eq(subject.current_url)
end
end
describe '#visit!' do
- include_context 'simple factory'
+ include_context 'simple resource'
before do
- allow(factory).to receive(:visit)
+ allow(resource).to receive(:visit)
end
it 'calls #visit with the underlying #web_url' do
- factory.web_url = subject.current_url
- factory.visit!
+ resource.web_url = subject.current_url
+ resource.visit!
- expect(factory).to have_received(:visit).with(subject.current_url)
+ expect(resource).to have_received(:visit).with(subject.current_url)
end
end
end
diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb
index 2eb6c008248..bf3ebce0cfe 100644
--- a/qa/spec/factory/repository/push_spec.rb
+++ b/qa/spec/resource/repository/push_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-describe QA::Factory::Repository::Push do
+describe QA::Resource::Repository::Push do
describe '.files=' do
let(:files) do
[
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index f7a4a4192d6..99429c93b82 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -32,12 +32,13 @@ describe Groups::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, group: group)
+ visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
- list_boards format: :html
+ list_boards
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 667eaa5e34f..8d503f6ad32 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -38,9 +38,10 @@ describe Projects::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, project: project)
+ visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
list_boards
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 85ba7d4097d..0cacdf7931f 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -27,6 +27,12 @@ FactoryBot.define do
pipeline factory: :ci_pipeline
+ trait :degenerated do
+ commands nil
+ options nil
+ yaml_variables nil
+ end
+
trait :started do
started_at 'Di 29. Okt 09:51:28 CET 2013'
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 70e0879dd81..4f8f67aab22 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -53,10 +53,21 @@ describe 'Environment' do
it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
- expect(page).to have_link('Re-deploy')
+ expect(page).not_to have_link('Re-deploy')
expect(page).not_to have_terminal_button
end
+ context 'when user has ability to re-deploy' do
+ let(:permissions) do
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it 'does show re-deploy' do
+ expect(page).to have_link('Re-deploy')
+ end
+ end
+
context 'with manual action' do
let(:action) do
create(:ci_build, :manual, pipeline: pipeline,
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 917ba495f01..22d0187ac81 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -162,7 +162,7 @@ describe 'Environments page', :js do
end
it 'shows a play button' do
- find('.js-dropdown-play-icon-container').click
+ find('.js-environment-actions-dropdown').click
expect(page).to have_content(action.name.humanize)
end
@@ -170,7 +170,7 @@ describe 'Environments page', :js do
it 'allows to play a manual action', :js do
expect(action).to be_manual
- find('.js-dropdown-play-icon-container').click
+ find('.js-environment-actions-dropdown').click
expect(page).to have_content(action.name.humanize)
expect { find('.js-manual-action-link').click }
@@ -260,6 +260,69 @@ describe 'Environments page', :js do
end
end
end
+
+ context 'when there is a delayed job' do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:delayed_job) do
+ create(:ci_build, :scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
+
+ let!(:deployment) do
+ create(:deployment,
+ environment: environment,
+ deployable: build,
+ sha: project.commit.id)
+ end
+
+ before do
+ visit_environments(project)
+ end
+
+ it 'has a dropdown for actionable jobs' do
+ expect(page).to have_selector('.dropdown-new.btn.btn-default .ic-play')
+ end
+
+ it "has link to the delayed job's action" do
+ find('.js-environment-actions-dropdown').click
+
+ expect(page).to have_button('Delayed job')
+ expect(page).to have_content(/\d{2}:\d{2}:\d{2}/)
+ end
+
+ context 'when delayed job is expired already' do
+ let!(:delayed_job) do
+ create(:ci_build, :expired_scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
+
+ it "shows 00:00:00 as the remaining time" do
+ find('.js-environment-actions-dropdown').click
+
+ expect(page).to have_content("00:00:00")
+ end
+ end
+
+ context 'when user played a delayed job immediately' do
+ before do
+ find('.js-environment-actions-dropdown').click
+ page.accept_confirm { click_button('Delayed job') }
+ wait_for_requests
+ end
+
+ it 'enqueues the delayed job', :js do
+ expect(delayed_job.reload).to be_pending
+ end
+ end
+ end
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cd6c37bf54d..049bbca958f 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -388,54 +388,83 @@ describe 'Pipeline', :js do
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
+ subject { visit pipeline_failures_page }
+
context 'with failed build' do
before do
failed_build.trace.set('4 examples, 1 failure')
-
- visit pipeline_failures_page
end
it 'shows jobs tab pane as active' do
+ subject
+
expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active')
end
it 'lists failed builds' do
+ subject
+
expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage)
end
it 'shows build failure logs' do
+ subject
+
expect(page).to have_content('4 examples, 1 failure')
end
it 'shows the failure reason' do
+ subject
+
expect(page).to have_content('There is an unknown failure, please try again')
end
- it 'shows retry button for failed build' do
- page.within(find('.build-failures', match: :first)) do
- expect(page).to have_link('Retry')
+ context 'when user does not have permission to retry build' do
+ it 'shows retry button for failed build' do
+ subject
+
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).not_to have_link('Retry')
+ end
end
end
- end
- context 'when missing build logs' do
- before do
- visit pipeline_failures_page
+ context 'when user does have permission to retry build' do
+ before do
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'shows retry button for failed build' do
+ subject
+
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).to have_link('Retry')
+ end
+ end
end
+ end
+ context 'when missing build logs' do
it 'shows jobs tab pane as active' do
+ subject
+
expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active')
end
it 'lists failed builds' do
+ subject
+
expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage)
end
it 'does not show trace' do
+ subject
+
expect(page).to have_content('No job trace')
end
end
@@ -448,11 +477,9 @@ describe 'Pipeline', :js do
end
context 'when accessing failed jobs page' do
- before do
- visit pipeline_failures_page
- end
-
it 'fails to access the page' do
+ subject
+
expect(page).to have_title('Access Denied')
end
end
@@ -461,11 +488,11 @@ describe 'Pipeline', :js do
context 'without failures' do
before do
failed_build.update!(status: :success)
-
- visit pipeline_failures_page
end
it 'displays the pipeline graph' do
+ subject
+
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.pipeline-visualization')
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index de9974c45e1..b51f1955ac4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -13,7 +13,7 @@ describe NotesFinder do
let!(:comment) { create(:note_on_issue, project: project) }
let!(:system_note) { create(:note_on_issue, project: project, system: true) }
- it 'filters system notes' do
+ it 'returns only user notes when using only_comments filter' do
finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments])
notes = finder.execute
@@ -21,6 +21,14 @@ describe NotesFinder do
expect(notes).to match_array(comment)
end
+ it 'returns only system notes when using only_activity filters' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_activity])
+
+ notes = finder.execute
+
+ expect(notes).to match_array(system_note)
+ end
+
it 'gets all notes' do
finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity])
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index 3f22b3a253d..3e849c9a644 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -92,7 +92,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with id' do
- subject { finder(params).find_by(id: active_personal_access_token.id) }
+ subject { finder(params).find_by_id(active_personal_access_token.id) }
it { is_expected.to eq(active_personal_access_token) }
@@ -106,7 +106,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with token' do
- subject { finder(params).find_by(token: active_personal_access_token.token) }
+ subject { finder(params).find_by_token(active_personal_access_token.token) }
it { is_expected.to eq(active_personal_access_token) }
@@ -207,7 +207,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with id' do
- subject { finder(params).find_by(id: active_personal_access_token.id) }
+ subject { finder(params).find_by_id(active_personal_access_token.id) }
it { is_expected.to eq(active_personal_access_token) }
@@ -221,7 +221,7 @@ describe PersonalAccessTokensFinder do
end
describe 'with token' do
- subject { finder(params).find_by(token: active_personal_access_token.token) }
+ subject { finder(params).find_by_token(active_personal_access_token.token) }
it { is_expected.to eq(active_personal_access_token) }
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index 44835386cfc..0828f113495 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -48,6 +48,10 @@
"manual_actions": {
"type": "array",
"items": { "$ref": "job/job.json" }
+ },
+ "scheduled_actions": {
+ "type": "array",
+ "items": { "$ref": "job/job.json" }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json
index 734c535ef70..f3d5e9b038a 100644
--- a/spec/fixtures/api/schemas/job/job.json
+++ b/spec/fixtures/api/schemas/job/job.json
@@ -9,7 +9,8 @@
"playable",
"created_at",
"updated_at",
- "status"
+ "status",
+ "archived"
],
"properties": {
"id": { "type": "integer" },
@@ -27,7 +28,8 @@
"updated_at": { "type": "string" },
"status": { "$ref": "../status/ci_detailed_status.json" },
"callout_message": { "type": "string" },
- "recoverable": { "type": "boolean" }
+ "recoverable": { "type": "boolean" },
+ "archived": { "type": "boolean" }
},
"additionalProperties": true
}
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index ffdf6561a53..ab4566e261b 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe TreeHelper do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
+ let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' }
describe '.render_tree' do
before do
@@ -32,6 +32,49 @@ describe TreeHelper do
end
end
+ describe '.fast_project_blob_path' do
+ it 'generates the same path as project_blob_path' do
+ blob_path = repository.tree(sha, 'with space').entries.first.path
+ fast_path = fast_project_blob_path(project, blob_path)
+ std_path = project_blob_path(project, blob_path)
+
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'generates the same path with encoded file names' do
+ tree = repository.tree(sha, 'encoding')
+ blob_path = tree.entries.find { |entry| entry.path == 'encoding/ćƒ†ć‚¹ćƒˆ.txt' }.path
+ fast_path = fast_project_blob_path(project, blob_path)
+ std_path = project_blob_path(project, blob_path)
+
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'respects a configured relative URL' do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
+ blob_path = repository.tree(sha, '').entries.first.path
+ fast_path = fast_project_blob_path(project, blob_path)
+
+ expect(fast_path).to start_with('/gitlab/root')
+ end
+ end
+
+ describe '.fast_project_tree_path' do
+ let(:tree_path) { repository.tree(sha, 'with space').path }
+ let(:fast_path) { fast_project_tree_path(project, tree_path) }
+ let(:std_path) { project_tree_path(project, tree_path) }
+
+ it 'generates the same path as project_tree_path' do
+ expect(fast_path).to eq(std_path)
+ end
+
+ it 'respects a configured relative URL' do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
+
+ expect(fast_path).to start_with('/gitlab/root')
+ end
+ end
+
describe 'flatten_tree' do
let(:tree) { repository.tree(sha, 'files') }
let(:root_path) { 'files' }
diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
index b7b29190c31..093fec97951 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
+++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
@@ -1,23 +1,35 @@
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import { setInput, createForm } from './helper';
+function expectToToggleDisableOnDirtyUpdate(submit, input) {
+ const originalValue = input.value;
+
+ expect(submit.disabled).toBe(true);
+
+ return setInput(input, `${originalValue} changes`)
+ .then(() => expect(submit.disabled).toBe(false))
+ .then(() => setInput(input, originalValue))
+ .then(() => expect(submit.disabled).toBe(true));
+}
+
describe('DirtySubmitForm', () => {
it('disables submit until there are changes', done => {
const { form, input, submit } = createForm();
- const originalValue = input.value;
new DirtySubmitForm(form); // eslint-disable-line no-new
- expect(submit.disabled).toBe(true);
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('disables submit until there are changes when initializing with a falsy value', done => {
+ const { form, input, submit } = createForm();
+ input.value = '';
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
- return setInput(input, `${originalValue} changes`)
- .then(() => {
- expect(submit.disabled).toBe(false);
- })
- .then(() => setInput(input, originalValue))
- .then(() => {
- expect(submit.disabled).toBe(true);
- })
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
.then(done)
.catch(done.fail);
});
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 223153d4e31..787df757d32 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -1,15 +1,19 @@
import Vue from 'vue';
-import actionsComp from '~/environments/components/environment_actions.vue';
+import eventHub from '~/environments/event_hub';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'spec/test_constants';
-describe('Actions Component', () => {
- let ActionsComponent;
- let actionsMock;
- let component;
+describe('EnvironmentActions Component', () => {
+ const Component = Vue.extend(EnvironmentActions);
+ let vm;
- beforeEach(() => {
- ActionsComponent = Vue.extend(actionsComp);
+ afterEach(() => {
+ vm.$destroy();
+ });
- actionsMock = [
+ describe('manual actions', () => {
+ const actions = [
{
name: 'bar',
play_path: 'https://gitlab.com/play',
@@ -25,43 +29,89 @@ describe('Actions Component', () => {
},
];
- component = new ActionsComponent({
- propsData: {
- actions: actionsMock,
- },
- }).$mount();
- });
+ beforeEach(() => {
+ vm = mountComponent(Component, { actions });
+ });
+
+ it('should render a dropdown button with icon and title attribute', () => {
+ expect(vm.$el.querySelector('.fa-caret-down')).toBeDefined();
+ expect(vm.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual(
+ 'Deploy to...',
+ );
- describe('computed', () => {
- it('title', () => {
- expect(component.title).toEqual('Deploy to...');
+ expect(vm.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual(
+ 'Deploy to...',
+ );
});
- });
- it('should render a dropdown button with icon and title attribute', () => {
- expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
- expect(
- component.$el.querySelector('.dropdown-new').getAttribute('data-original-title'),
- ).toEqual('Deploy to...');
+ it('should render a dropdown with the provided list of actions', () => {
+ expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actions.length);
+ });
- expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual(
- 'Deploy to...',
- );
- });
+ it("should render a disabled action when it's not playable", () => {
+ expect(
+ vm.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
+ ).toEqual('disabled');
- it('should render a dropdown with the provided list of actions', () => {
- expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(actionsMock.length);
+ expect(
+ vm.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'),
+ ).toEqual(true);
+ });
});
- it("should render a disabled action when it's not playable", () => {
- expect(
- component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
- ).toEqual('disabled');
+ describe('scheduled jobs', () => {
+ const scheduledJobAction = {
+ name: 'scheduled action',
+ playPath: `${TEST_HOST}/scheduled/job/action`,
+ playable: true,
+ scheduledAt: '2063-04-05T00:42:00Z',
+ };
+ const expiredJobAction = {
+ name: 'expired action',
+ playPath: `${TEST_HOST}/expired/job/action`,
+ playable: true,
+ scheduledAt: '2018-10-05T08:23:00Z',
+ };
+ const findDropdownItem = action => {
+ const buttons = vm.$el.querySelectorAll('.dropdown-menu li button');
+ return Array.prototype.find.call(buttons, element =>
+ element.innerText.trim().startsWith(action.name),
+ );
+ };
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
+ vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
+ });
+
+ it('emits postAction event after confirming', () => {
+ const emitSpy = jasmine.createSpy('emit');
+ eventHub.$on('postAction', emitSpy);
+ spyOn(window, 'confirm').and.callFake(() => true);
+
+ findDropdownItem(scheduledJobAction).click();
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
+ });
+
+ it('does not emit postAction event if confirmation is cancelled', () => {
+ const emitSpy = jasmine.createSpy('emit');
+ eventHub.$on('postAction', emitSpy);
+ spyOn(window, 'confirm').and.callFake(() => false);
+
+ findDropdownItem(scheduledJobAction).click();
- expect(
- component.$el
- .querySelector('.dropdown-menu li:last-child button')
- .classList.contains('disabled'),
- ).toEqual(true);
+ expect(window.confirm).toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00');
+ });
+
+ it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00');
+ });
});
});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
index a81bdf618a3..9070d968cfd 100644
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -19,7 +19,7 @@ describe('DiscussionFilter component', () => {
},
];
const Component = Vue.extend(DiscussionFilter);
- const defaultValue = discussionFiltersMock[0].value;
+ const selectedValue = discussionFiltersMock[0].value;
store.state.discussions = discussions;
vm = mountComponentWithStore(Component, {
@@ -27,7 +27,7 @@ describe('DiscussionFilter component', () => {
store,
props: {
filters: discussionFiltersMock,
- defaultValue,
+ selectedValue,
},
});
});
@@ -63,4 +63,24 @@ describe('DiscussionFilter component', () => {
expect(vm.filterDiscussion).not.toHaveBeenCalled();
});
+
+ it('disables commenting when "Show history only" filter is applied', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ filterItem.click();
+
+ expect(vm.$store.state.commentsDisabled).toBe(true);
+ });
+
+ it('enables commenting when "Show history only" filter is not applied', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ filterItem.click();
+
+ expect(vm.$store.state.commentsDisabled).toBe(false);
+ });
+
+ it('renders a dropdown divider for the default filter', () => {
+ const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child');
+
+ expect(defaultFilter.lastChild.classList).toContain('dropdown-divider');
+ });
});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 3e289a6b8e6..0081f42c330 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -121,6 +121,13 @@ describe('note_app', () => {
).toEqual('Write a comment or drag your files here…');
});
+ it('should not render form when commenting is disabled', () => {
+ store.state.commentsDisabled = true;
+ vm = mountComponent();
+
+ expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null);
+ });
+
it('should render form comment button as disabled', () => {
expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual(
'disabled',
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index f4643fd55ed..0c0bc45b201 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -509,4 +509,17 @@ describe('Actions Notes Store', () => {
expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated');
});
});
+
+ describe('setCommentsDisabled', () => {
+ it('should set comments disabled state', done => {
+ testAction(
+ actions.setCommentsDisabled,
+ true,
+ null,
+ [{ type: 'DISABLE_COMMENTS', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 380ab59099d..461de5a3106 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -427,4 +427,14 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].expanded).toBe(true);
});
});
+
+ describe('DISABLE_COMMENTS', () => {
+ it('should set comments disabled state', () => {
+ const state = {};
+
+ mutations.DISABLE_COMMENTS(state, true);
+
+ expect(state.commentsDisabled).toEqual(true);
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 8095a231cf3..1140bfdf6c3 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
where(:keyword, :file) do
:junit | 'junit.xml'
- :codequality | 'codequality.json'
+ :codequality | 'gl-code-quality-report.json'
:sast | 'gl-sast-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a63f34b5536..f4efa450cca 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -299,6 +299,7 @@ project:
- ci_cd_settings
- import_export_upload
- repository_languages
+- pool_repository
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index b333b334f36..c92bc92c42d 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should generate the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('alpine:3.6')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.7.2-kube-1.11.0')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 87b91286168..95ae7bd21ab 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -594,4 +594,24 @@ describe ApplicationSetting do
end
end
end
+
+ describe '#archive_builds_older_than' do
+ subject { setting.archive_builds_older_than }
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = 3600
+ end
+
+ it { is_expected.to be_within(1.minute).of(1.hour.ago) }
+ end
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 13a4aaa8936..4089f099fdf 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1314,6 +1314,14 @@ describe Ci::Build do
it { is_expected.not_to be_retryable }
end
+
+ context 'when build is degenerated' do
+ before do
+ build.degenerate!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -1396,6 +1404,14 @@ describe Ci::Build do
expect(subject.retries_max).to eq 0
end
end
+
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
+
+ it 'returns zero' do
+ expect(subject.retries_max).to eq 0
+ end
+ end
end
end
@@ -1511,11 +1527,11 @@ describe Ci::Build do
end
end
- describe '#other_actions' do
+ describe '#other_manual_actions' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
- subject { build.other_actions }
+ subject { build.other_manual_actions }
before do
project.add_developer(user)
@@ -1546,6 +1562,48 @@ describe Ci::Build do
end
end
+ describe '#other_scheduled_actions' do
+ let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) }
+
+ subject { build.other_scheduled_actions }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context "when other build's status is success" do
+ let!(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other action') }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+
+ context "when other build's status is failed" do
+ let!(:other_build) { create(:ci_build, :schedulable, :failed, pipeline: pipeline, name: 'other action') }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+
+ context "when other build's status is running" do
+ let!(:other_build) { create(:ci_build, :schedulable, :running, pipeline: pipeline, name: 'other action') }
+
+ it 'does not return other actions' do
+ is_expected.to be_empty
+ end
+ end
+
+ context "when other build's status is scheduled" do
+ let!(:other_build) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'other action') }
+
+ it 'does not return other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+ end
+ end
+
describe '#persisted_environment' do
let!(:environment) do
create(:environment, project: project, name: "foo-#{project.default_branch}")
@@ -1617,6 +1675,12 @@ describe Ci::Build do
it { is_expected.to be_playable }
end
+
+ context 'when build is a manual and degenerated' do
+ subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
+
+ it { is_expected.not_to be_playable }
+ end
end
context 'when build is scheduled' do
@@ -3185,4 +3249,54 @@ describe Ci::Build do
it { expect(build.deployment_status).to eq(:creating) }
end
end
+
+ describe '#degenerated?' do
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
+
+ it { is_expected.to be_degenerated }
+ end
+
+ context 'when build is valid' do
+ subject { create(:ci_build) }
+
+ it { is_expected.not_to be_degenerated }
+
+ context 'and becomes degenerated' do
+ before do
+ subject.degenerate!
+ end
+
+ it { is_expected.to be_degenerated }
+ end
+ end
+ end
+
+ describe '#archived?' do
+ context 'when build is degenerated' do
+ subject { create(:ci_build, :degenerated) }
+
+ it { is_expected.to be_archived }
+ end
+
+ context 'for old build' do
+ subject { create(:ci_build, created_at: 1.day.ago) }
+
+ context 'when archive_builds_in is set' do
+ before do
+ stub_application_setting(archive_builds_in_seconds: 3600)
+ end
+
+ it { is_expected.to be_archived }
+ end
+
+ context 'when archive_builds_in is not set' do
+ before do
+ stub_application_setting(archive_builds_in_seconds: nil)
+ end
+
+ it { is_expected.not_to be_archived }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 153244b2159..0f58c06864e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1148,6 +1148,19 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.latest_successful_ids_per_project' do
+ let(:projects) { create_list(:project, 2) }
+ let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) }
+ let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) }
+ let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) }
+ let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) }
+
+ it 'returns expected pipeline ids' do
+ expect(described_class.latest_successful_ids_per_project)
+ .to contain_exactly(pipeline2, pipeline4)
+ end
+ end
+
describe '.internal_sources' do
subject { described_class.internal_sources }
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index debc02fa51f..5713106418d 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -37,8 +37,8 @@ describe Awardable do
create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user)
create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user)
- expect(Issue.awarded(award_emoji.user)).to eq [issue, issue3]
- expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3]
+ expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3)
+ expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3)
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b8364e0cf88..146d35122f7 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,22 @@ describe Deployment do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe '#scheduled_actions' do
+ subject { deployment.scheduled_actions }
+
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ it 'delegates to other_scheduled_actions' do
+ expect_any_instance_of(Ci::Build)
+ .to receive(:other_scheduled_actions)
+
+ subject
+ end
+ end
+
describe 'modules' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 562ccaf6c0b..47e8f04e728 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -211,4 +211,25 @@ describe MergeRequestDiff do
expect(diff_with_commits.commits_count).to eq(29)
end
end
+
+ describe '#commits_by_shas' do
+ let(:commit_shas) { diff_with_commits.commit_shas }
+
+ it 'returns empty if no SHAs were provided' do
+ expect(diff_with_commits.commits_by_shas([])).to be_empty
+ end
+
+ it 'returns one SHA' do
+ commits = diff_with_commits.commits_by_shas([commit_shas.first, Gitlab::Git::BLANK_SHA])
+
+ expect(commits.count).to eq(1)
+ end
+
+ it 'returns all matching SHAs' do
+ commits = diff_with_commits.commits_by_shas(commit_shas)
+
+ expect(commits.count).to eq(commit_shas.count)
+ expect(commits.map(&:sha)).to match_array(commit_shas)
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 7d500f9e579..2eb5e39ccfd 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2611,6 +2611,32 @@ describe MergeRequest do
end
end
+ describe '#includes_any_commits?' do
+ it 'returns false' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ it 'returns true even when there is a non-existent comit' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ context 'unpersisted merge request' do
+ let(:new_mr) { build(:merge_request) }
+
+ it 'returns false' do
+ expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+ end
+ end
+
describe '#can_allow_collaboration?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 8913644a3ce..2db42fe802a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -562,6 +562,17 @@ describe Namespace do
it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
end
+ describe '#all_pipelines' do
+ let(:group) { create(:group) }
+ let(:child) { create(:group, parent: group) }
+ let!(:project1) { create(:project_empty_repo, namespace: group) }
+ let!(:project2) { create(:project_empty_repo, namespace: child) }
+ let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+
+ it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) }
+ end
+
describe '#share_with_group_lock with subgroups', :nested_groups do
context 'when creating a subgroup' do
let(:subgroup) { create(:group, parent: root_group )}
diff --git a/spec/models/postgresql/replication_slot_spec.rb b/spec/models/postgresql/replication_slot_spec.rb
index 919a7526803..e100af7ddc7 100644
--- a/spec/models/postgresql/replication_slot_spec.rb
+++ b/spec/models/postgresql/replication_slot_spec.rb
@@ -3,7 +3,27 @@
require 'spec_helper'
describe Postgresql::ReplicationSlot, :postgresql do
+ describe '.in_use?' do
+ it 'returns true when replication slots are present' do
+ expect(described_class).to receive(:exists?).and_return(true)
+ expect(described_class.in_use?).to be_truthy
+ end
+
+ it 'returns false when replication slots are not present' do
+ expect(described_class.in_use?).to be_falsey
+ end
+
+ it 'returns false if the existence check is invalid' do
+ expect(described_class).to receive(:exists?).and_raise(ActiveRecord::StatementInvalid.new('PG::FeatureNotSupported'))
+ expect(described_class.in_use?).to be_falsey
+ end
+ end
+
describe '.lag_too_great?' do
+ before do
+ expect(described_class).to receive(:in_use?).and_return(true)
+ end
+
it 'returns true when replication lag is too great' do
expect(described_class)
.to receive(:pluck)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d059854214f..84326724118 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -8,6 +8,7 @@ describe Project do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:creator).class_name('User') }
+ it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
it { is_expected.to have_many(:services) }
it { is_expected.to have_many(:events) }
diff --git a/spec/models/shard_spec.rb b/spec/models/shard_spec.rb
new file mode 100644
index 00000000000..83104711b55
--- /dev/null
+++ b/spec/models/shard_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literals: true
+require 'spec_helper'
+
+describe Shard do
+ describe '.populate!' do
+ it 'creates shards based on the config file' do
+ expect(described_class.all).to be_empty
+
+ stub_storage_settings(foo: {}, bar: {}, baz: {})
+
+ described_class.populate!
+
+ expect(described_class.all.map(&:name)).to match_array(%w[default foo bar baz])
+ end
+ end
+
+ describe '.by_name' do
+ let(:default_shard) { described_class.find_by(name: 'default') }
+
+ before do
+ described_class.populate!
+ end
+
+ it 'returns an existing shard' do
+ expect(described_class.by_name('default')).to eq(default_shard)
+ end
+
+ it 'creates a new shard' do
+ result = described_class.by_name('foo')
+
+ expect(result).not_to eq(default_shard)
+ expect(result.name).to eq('foo')
+ end
+
+ it 'retries if creation races' do
+ expect(described_class)
+ .to receive(:find_or_create_by)
+ .with(name: 'default')
+ .and_raise(ActiveRecord::RecordNotUnique, 'fail')
+ .once
+
+ expect(described_class)
+ .to receive(:find_or_create_by)
+ .with(name: 'default')
+ .and_call_original
+
+ expect(described_class.by_name('default')).to eq(default_shard)
+ end
+ end
+end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 64d9d9a78b4..2898613545c 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -6,22 +6,43 @@ describe UserPreference do
describe '#set_notes_filter' do
let(:issuable) { build_stubbed(:issue) }
let(:user_preference) { create(:user_preference) }
- let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
- it 'returns updated discussion filter' do
- filter_name =
- user_preference.set_notes_filter(only_comments, issuable)
+ shared_examples 'setting system notes' do
+ it 'returns updated discussion filter' do
+ filter_name =
+ user_preference.set_notes_filter(filter, issuable)
+
+ expect(filter_name).to eq(filter)
+ end
+
+ it 'updates discussion filter for issuable class' do
+ user_preference.set_notes_filter(filter, issuable)
+
+ expect(user_preference.reload.issue_notes_filter).to eq(filter)
+ end
+ end
+
+ context 'when filter is set to all notes' do
+ let(:filter) { described_class::NOTES_FILTERS[:all_notes] }
+
+ it_behaves_like 'setting system notes'
+ end
+
+ context 'when filter is set to only comments' do
+ let(:filter) { described_class::NOTES_FILTERS[:only_comments] }
- expect(filter_name).to eq(only_comments)
+ it_behaves_like 'setting system notes'
end
- it 'updates discussion filter for issuable class' do
- user_preference.set_notes_filter(only_comments, issuable)
+ context 'when filter is set to only activity' do
+ let(:filter) { described_class::NOTES_FILTERS[:only_activity] }
- expect(user_preference.reload.issue_notes_filter).to eq(only_comments)
+ it_behaves_like 'setting system notes'
end
context 'when notes_filter parameter is invalid' do
+ let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
+
it 'returns the current notes filter' do
user_preference.set_notes_filter(only_comments, issuable)
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index d7992f0a4a9..676835b3880 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do
let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do
- let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
+ let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
it 'should return false' do
failure_reasons.each do |failure_reason|
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 522c92ce295..8793a762f9d 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -22,4 +22,26 @@ describe DeploymentEntity do
it 'exposes creation date' do
expect(subject).to include(:created_at)
end
+
+ describe 'scheduled_actions' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ context 'when the same pipeline has a scheduled action' do
+ let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') }
+ let!(:other_deployment) { create(:deployment, deployable: other_build) }
+
+ it 'returns other scheduled actions' do
+ expect(subject[:scheduled_actions][0][:name]).to eq 'other build'
+ end
+ end
+
+ context 'when the same pipeline does not have a scheduled action' do
+ it 'does not return other actions' do
+ expect(subject[:scheduled_actions]).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index a6565709641..56e2a405bcd 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -478,6 +478,20 @@ module Ci
it_behaves_like 'validation is not active'
end
end
+
+ context 'when build is degenerated' do
+ let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
+
+ subject { execute(specific_runner, {}) }
+
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_archived_failure
+ end
+ end
end
describe '#register_success' do
diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
index 9c9d7ad781e..95e69328080 100644
--- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
@@ -34,12 +34,24 @@ shared_examples 'issuable notes filter' do
expect(user.reload.notes_filter_for(issuable)).to eq(0)
end
- it 'returns no system note' do
+ it 'returns only user comments' do
user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ discussions = JSON.parse(response.body)
- expect(JSON.parse(response.body).count).to eq(1)
+ expect(discussions.count).to eq(1)
+ expect(discussions.first["notes"].first["system"]).to be(false)
+ end
+
+ it 'returns only activity notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ discussions = JSON.parse(response.body)
+
+ expect(discussions.count).to eq(1)
+ expect(discussions.first["notes"].first["system"]).to be(true)
end
context 'when filter is set to "only_comments"' do
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index ef9bb7f5533..361d4220c6e 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -3,12 +3,6 @@ shared_examples 'helm commands' do
let(:helm_setup) do
<<~EOS
set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.2-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
EOS
end
diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
index 6a477c712ff..3353b7665e2 100644
--- a/spec/views/projects/tree/_blob_item.html.haml_spec.rb
+++ b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/tree/_blob_item' do
+describe 'projects/tree/_tree_row' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
@@ -31,10 +31,7 @@ describe 'projects/tree/_blob_item' do
end
end
- def render_partial(blob_item)
- render partial: 'projects/tree/blob_item', locals: {
- blob_item: blob_item,
- type: 'blob'
- }
+ def render_partial(items)
+ render partial: 'projects/tree/tree_row', collection: [items].flatten
end
end