Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-16 21:09:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-16 21:09:24 +0300
commit1eec6b22b26d09ce6927adf66f98d755a6339815 (patch)
treeb1bb8bbdc0d49136bfd176a1e64d3bd9f2969396
parentb4e854a900ba9bcbfc3476f88317c59ea048daaf (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/verify-lockfile.gitlab-ci.yml11
-rw-r--r--app/assets/javascripts/clone_panel.js5
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js150
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js10
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js88
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue10
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue4
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js12
-rw-r--r--app/assets/javascripts/merge_request_tabs.js19
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/status/index.js (renamed from app/assets/javascripts/pages/import/bulk_imports/index.js)0
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/controllers/import/bulk_imports_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/repositories/git_http_controller.rb2
-rw-r--r--app/finders/ci/jobs_finder.rb3
-rw-r--r--app/finders/deployments_finder.rb25
-rw-r--r--app/graphql/mutations/merge_requests/update.rb11
-rw-r--r--app/graphql/types/merge_request_state_event_enum.rb16
-rw-r--r--app/helpers/labels_helper.rb42
-rw-r--r--app/models/clusters/agent_token.rb1
-rw-r--r--app/models/commit_status.rb24
-rw-r--r--app/models/deployment.rb8
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/services/bulk_import_service.rb2
-rw-r--r--app/services/merge_requests/reload_merge_head_diff_service.rb2
-rw-r--r--app/views/import/bulk_imports/status.html.haml1
-rw-r--r--app/views/projects/buttons/_clone.html.haml17
-rw-r--r--app/views/projects/buttons/_xcode_link.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml5
-rw-r--r--app/views/shared/notifications/_new_button.html.haml6
-rw-r--r--app/workers/pages_transfer_worker.rb2
-rw-r--r--changelogs/unreleased/220957-Open-with-IDE.yml5
-rw-r--r--changelogs/unreleased/273067-fix-ide-open-mr-highligh-first-file.yml5
-rw-r--r--changelogs/unreleased/276917-remove-default_merge_ref_for_diffs-feature-flag.yml5
-rw-r--r--changelogs/unreleased/293720-token-created-by.yml5
-rw-r--r--changelogs/unreleased/298801-group-migration-show-import-status-frontend.yml5
-rw-r--r--changelogs/unreleased/ApplyGitLabUIstylesin_notifications_directory.yml5
-rw-r--r--changelogs/unreleased/ajk-add-state-events-to-mr-update-mutation.yml5
-rw-r--r--changelogs/unreleased/ar-color-name-changes.yml5
-rw-r--r--changelogs/unreleased/leipert-remove-sticky-polyfill.yml5
-rw-r--r--changelogs/unreleased/return-all-jobs.yml5
-rw-r--r--config/feature_flags/development/default_merge_ref_for_diffs.yml2
-rw-r--r--config/feature_flags/development/disable_git_http_fetch_writes.yml8
-rw-r--r--config/feature_flags/development/disable_ssh_key_used_tracking.yml8
-rw-r--r--config/feature_flags/development/gitlab_ci_archived_trace_consistent_reads.yml8
-rw-r--r--config/feature_flags/development/simplified_commit_status_group_name.yml8
-rw-r--r--db/migrate/20210211195543_add_created_by_user_for_cluster_agent_token.rb28
-rw-r--r--db/schema_migrations/202102111955431
-rw-r--r--db/structure.sql6
-rw-r--r--doc/administration/pages/index.md20
-rw-r--r--doc/api/dora4_group_analytics.md84
-rw-r--r--doc/api/dora4_project_analytics.md54
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql27
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json49
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/api/group_import_export.md1
-rw-r--r--doc/api/jobs.md18
-rw-r--r--doc/api/project_analytics.md52
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/ci_cd_for_external_repos/bitbucket_integration.md2
-rw-r--r--doc/ci/cloud_deployment/index.md6
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/docker/using_docker_images.md18
-rw-r--r--doc/ci/environments/deployment_safety.md2
-rw-r--r--doc/ci/environments/index.md18
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md6
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md8
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md2
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md2
-rw-r--r--doc/ci/jobs/index.md19
-rw-r--r--doc/ci/migration/circleci.md2
-rw-r--r--doc/ci/multi_project_pipelines.md10
-rw-r--r--doc/ci/parent_child_pipelines.md4
-rw-r--r--doc/ci/pipelines/job_artifacts.md2
-rw-r--r--doc/ci/review_apps/index.md4
-rw-r--r--doc/ci/secrets/index.md8
-rw-r--r--doc/ci/services/mysql.md2
-rw-r--r--doc/ci/services/postgres.md2
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md22
-rw-r--r--doc/development/documentation/feature_flags.md2
-rw-r--r--doc/development/documentation/styleguide/img/tier_badge.pngbin9592 -> 9320 bytes
-rw-r--r--doc/user/analytics/ci_cd_analytics.md4
-rw-r--r--doc/user/group/index.md12
-rw-r--r--doc/user/group/settings/import_export.md1
-rw-r--r--doc/user/packages/composer_repository/index.md4
-rw-r--r--doc/user/packages/conan_repository/index.md4
-rw-r--r--doc/user/packages/dependency_proxy/index.md2
-rw-r--r--doc/user/packages/go_proxy/index.md4
-rw-r--r--doc/user/packages/maven_repository/index.md4
-rw-r--r--doc/user/packages/npm_registry/index.md4
-rw-r--r--doc/user/packages/nuget_repository/index.md4
-rw-r--r--doc/user/packages/pypi_repository/index.md4
-rw-r--r--doc/user/project/index.md6
-rw-r--r--doc/user/project/repository/index.md25
-rw-r--r--lib/api/ci/pipelines.rb1
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/support/git_access_actor.rb2
-rw-r--r--lib/gitlab/ci/trace.rb20
-rw-r--r--lib/gitlab/pages_transfer.rb14
-rw-r--r--locale/gitlab.pot67
-rw-r--r--package.json1
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb4
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb27
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb33
-rw-r--r--spec/finders/deployments_finder_spec.rb219
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js4
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb19
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js105
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js18
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js250
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js971
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js42
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js8
-rw-r--r--spec/frontend_integration/ide/helpers/start.js9
-rw-r--r--spec/frontend_integration/ide/user_opens_mr_spec.js60
-rw-r--r--spec/frontend_integration/test_helpers/fixtures.js6
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/index.js6
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/404.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/projects.js18
-rw-r--r--spec/graphql/mutations/merge_requests/update_spec.rb21
-rw-r--r--spec/graphql/types/merge_request_state_event_enum_spec.rb12
-rw-r--r--spec/lib/api/support/git_access_actor_spec.rb12
-rw-r--r--spec/lib/gitlab/pages_transfer_spec.rb22
-rw-r--r--spec/models/clusters/agent_token_spec.rb1
-rw-r--r--spec/models/commit_status_spec.rb56
-rw-r--r--spec/models/deployment_spec.rb36
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb34
-rw-r--r--spec/requests/api/repositories_spec.rb2
-rw-r--r--spec/serializers/ci/dag_pipeline_entity_spec.rb6
-rw-r--r--spec/services/ci/daily_build_group_report_result_service_spec.rb17
-rw-r--r--spec/workers/pages_transfer_worker_spec.rb2
-rw-r--r--yarn.lock5
145 files changed, 2054 insertions, 1316 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e38e2f765bd..530faada428 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -111,4 +111,5 @@ include:
- local: .gitlab/ci/dast.gitlab-ci.yml
- local: .gitlab/ci/workhorse.gitlab-ci.yml
- local: .gitlab/ci/graphql.gitlab-ci.yml
- - local: .gitlab/ci/verify-lockfile.gitlab-ci.yml
+ - project: 'gitlab-org/frontend/untamper-my-lockfile'
+ file: '.gitlab-ci-template.yml'
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index fc6c005a13a..22aa92779ea 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -194,6 +194,8 @@ update-rails-cache:
extends:
- update-setup-test-env-cache
- .rails-cache
+ cache:
+ policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
.coverage-base:
extends:
diff --git a/.gitlab/ci/verify-lockfile.gitlab-ci.yml b/.gitlab/ci/verify-lockfile.gitlab-ci.yml
deleted file mode 100644
index 6336a428b4b..00000000000
--- a/.gitlab/ci/verify-lockfile.gitlab-ci.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-verify_lockfile:
- stage: test
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-git-2.29-lfs-2.9-node-14.15-yarn-1.22-graphicsmagick-1.3.34
- needs: []
- rules:
- - changes:
- - yarn.lock
- script:
- - npm config set @dappelt:registry https://gitlab.com/api/v4/projects/22564149/packages/npm/
- - npx lockfile-lint@4.3.7 --path yarn.lock --allowed-hosts yarn --validate-https
- - npx @dappelt/untamper-my-lockfile --lockfile yarn.lock
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index 00bf54e1478..c9fae8f17a4 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -18,6 +18,11 @@ export default function initClonePanel() {
e.preventDefault();
const $this = $(e.currentTarget);
const url = $this.attr('href');
+ if (url && (url.startsWith('vscode://') || url.startsWith('xcode://'))) {
+ // Clone with "..." should open like a normal link
+ return;
+ }
+ e.preventDefault();
const cloneType = $this.data('cloneType');
$('.is-active', $cloneOptions).removeClass('is-active');
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index bd8d2d6b8f2..6b1e2bfb34e 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { polyfillSticky } from '~/lib/utils/sticky';
import { __ } from '~/locale';
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
import eventHub from '../event_hub';
@@ -61,9 +60,6 @@ export default {
created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
- mounted() {
- polyfillSticky(this.$el);
- },
methods: {
...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'setShowTreeList']),
expandAllFiles() {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 6bd74b143e2..ed6b750480b 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -107,3 +107,7 @@ export const SIDE_RIGHT = 'right';
// Live Preview feature
export const LIVE_PREVIEW_DEBOUNCE = 2000;
+
+// This is the maximum number of files to auto open when opening the Web IDE
+// from a Merge Request
+export const MAX_MR_FILES_AUTO_OPEN = 10;
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index aa2e3d32b59..d1e40920ebc 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -120,10 +120,6 @@ export const getFileData = (
});
};
-export const setFileMrChange = ({ commit }, { file, mrChange }) => {
- commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
-};
-
export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path];
const stagedFile = state.stagedFiles.find((f) => f.path === path);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 8dcc420f156..753f6b9cd47 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,6 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
-import { leftSidebarViews, PERMISSION_READ_MR } from '../../constants';
+import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants';
import service from '../../services';
import * as types from '../mutation_types';
@@ -147,70 +147,96 @@ export const getMergeRequestVersions = (
}
});
-export const openMergeRequest = (
- { dispatch, state, getters },
- { projectId, targetProjectId, mergeRequestId } = {},
-) =>
- dispatch('getMergeRequestData', {
- projectId,
- targetProjectId,
- mergeRequestId,
- })
- .then((mr) => {
- dispatch('setCurrentBranchId', mr.source_branch);
-
- return dispatch('getBranchData', {
- projectId,
- branchId: mr.source_branch,
- }).then(() => {
- const branch = getters.findBranch(projectId, mr.source_branch);
-
- return dispatch('getFiles', {
- projectId,
- branchId: mr.source_branch,
- ref: branch.commit.id,
+export const openMergeRequestChanges = async ({ dispatch, getters, state, commit }, changes) => {
+ const entryChanges = changes
+ .map((change) => ({ entry: state.entries[change.new_path], change }))
+ .filter((x) => x.entry);
+
+ const pathsToOpen = entryChanges
+ .slice(0, MAX_MR_FILES_AUTO_OPEN)
+ .map(({ change }) => change.new_path);
+
+ // If there are no changes with entries, do nothing.
+ if (!entryChanges.length) {
+ return;
+ }
+
+ dispatch('updateActivityBarView', leftSidebarViews.review.name);
+
+ entryChanges.forEach(({ change, entry }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file: entry, mrChange: change });
+ });
+
+ // Open paths in order as they appear in MR changes
+ pathsToOpen.forEach((path) => {
+ commit(types.TOGGLE_FILE_OPEN, path);
+ });
+
+ // Activate first path.
+ // We don't `getFileData` here since the editor component kicks that off. Otherwise, we'd fetch twice.
+ const [firstPath, ...remainingPaths] = pathsToOpen;
+ await dispatch('router/push', getters.getUrlForPath(firstPath));
+ await dispatch('setFileActive', firstPath);
+
+ // Lastly, eagerly fetch the remaining paths for improved user experience.
+ await Promise.all(
+ remainingPaths.map(async (path) => {
+ try {
+ await dispatch('getFileData', {
+ path,
+ makeFileActive: false,
});
- });
- })
- .then(() =>
- dispatch('getMergeRequestVersions', {
- projectId,
- targetProjectId,
- mergeRequestId,
- }),
- )
- .then(() =>
- dispatch('getMergeRequestChanges', {
- projectId,
- targetProjectId,
- mergeRequestId,
- }),
- )
- .then((mrChanges) => {
- if (mrChanges.changes.length) {
- dispatch('updateActivityBarView', leftSidebarViews.review.name);
+ await dispatch('getRawFileData', { path });
+ } catch (e) {
+ // If one of the file fetches fails, we dont want to blow up the rest of them.
+ // eslint-disable-next-line no-console
+ console.error('[gitlab] An unexpected error occurred fetching MR file data', e);
}
+ }),
+ );
+};
- mrChanges.changes.forEach((change, ind) => {
- const changeTreeEntry = state.entries[change.new_path];
+export const openMergeRequest = async (
+ { dispatch, getters },
+ { projectId, targetProjectId, mergeRequestId } = {},
+) => {
+ try {
+ const mr = await dispatch('getMergeRequestData', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
- if (changeTreeEntry) {
- dispatch('setFileMrChange', {
- file: changeTreeEntry,
- mrChange: change,
- });
+ dispatch('setCurrentBranchId', mr.source_branch);
- if (ind < 10) {
- dispatch('getFileData', {
- path: change.new_path,
- makeFileActive: ind === 0,
- openFile: true,
- });
- }
- }
- });
- })
- .catch((e) => {
- flash(__('Error while loading the merge request. Please try again.'));
- throw e;
+ await dispatch('getBranchData', {
+ projectId,
+ branchId: mr.source_branch,
+ });
+
+ const branch = getters.findBranch(projectId, mr.source_branch);
+
+ await dispatch('getFiles', {
+ projectId,
+ branchId: mr.source_branch,
+ ref: branch.commit.id,
});
+
+ await dispatch('getMergeRequestVersions', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
+
+ const { changes } = await dispatch('getMergeRequestChanges', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
+
+ await dispatch('openMergeRequestChanges', changes);
+ } catch (e) {
+ flash(__('Error while loading the merge request. Please try again.'));
+ throw e;
+ }
+};
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 651fc907611..8110934efc4 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -25,6 +25,14 @@ export function createResolvers({ endpoints }) {
data: { availableNamespaces },
} = await client.query({ query: availableNamespacesQuery });
+ if (!statusPoller) {
+ statusPoller = new StatusPoller({
+ client,
+ pollPath: endpoints.jobs,
+ });
+ statusPoller.startPolling();
+ }
+
return axios
.get(endpoints.status, {
params: {
@@ -83,7 +91,7 @@ export function createResolvers({ endpoints }) {
const group = groupManager.findById(sourceGroupId);
groupManager.setImportStatus(group, STATUSES.SCHEDULING);
try {
- await axios.post(endpoints.createBulkImport, {
+ const response = await axios.post(endpoints.createBulkImport, {
bulk_import: [
{
source_type: 'group_entity',
@@ -94,10 +102,7 @@ export function createResolvers({ endpoints }) {
],
});
groupManager.setImportStatus(group, STATUSES.STARTED);
- if (!statusPoller) {
- statusPoller = new StatusPoller({ client, interval: 3000 });
- statusPoller.startPolling();
- }
+ SourceGroupsManager.attachImportId(group, response.data.id);
} catch (e) {
createFlash({
message: s__('BulkImport|Importing the group failed'),
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
index 047b04fe7d6..261e30edbbb 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -14,6 +14,12 @@ function generateGroupId(id) {
}
export class SourceGroupsManager {
+ static importMap = new Map();
+
+ static attachImportId(group, importId) {
+ SourceGroupsManager.importMap.set(importId, group.id);
+ }
+
constructor({ client }) {
this.client = client;
}
@@ -36,6 +42,10 @@ export class SourceGroupsManager {
this.update(group, fn);
}
+ findByImportId(importId) {
+ return this.findById(SourceGroupsManager.importMap.get(importId));
+ }
+
setImportStatus(group, status) {
this.update(group, (sourceGroup) => {
// eslint-disable-next-line no-param-reassign
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
index 960126cfa6d..63cd6b48fc4 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
@@ -1,71 +1,47 @@
-import gql from 'graphql-tag';
+import Visibility from 'visibilityjs';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
-import { STATUSES } from '../../../constants';
-import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.query.graphql';
import { SourceGroupsManager } from './source_groups_manager';
-const groupId = (i) => `group${i}`;
-
-function generateGroupsQuery(groups) {
- return gql`{
- ${groups
- .map(
- (g, idx) =>
- `${groupId(idx)}: group(fullPath: "${g.import_target.target_namespace}/${
- g.import_target.new_name
- }") { id }`,
- )
- .join('\n')}
- }`;
-}
-
export class StatusPoller {
- constructor({ client, interval }) {
+ constructor({ client, pollPath }) {
this.client = client;
- this.interval = interval;
- this.timeoutId = null;
- this.groupManager = new SourceGroupsManager({ client });
- }
- startPolling() {
- if (this.timeoutId) {
- return;
- }
+ this.eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(pollPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data }) => this.updateImportsStatuses(data),
+ errorCallback: () =>
+ createFlash({
+ message: s__('BulkImport|Update of import statuses with realtime changes failed'),
+ }),
+ });
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.eTagPoll.restart();
+ } else {
+ this.eTagPoll.stop();
+ }
+ });
- this.checkPendingImports();
+ this.groupManager = new SourceGroupsManager({ client });
}
- stopPolling() {
- clearTimeout(this.timeoutId);
- this.timeoutId = null;
+ startPolling() {
+ this.eTagPoll.makeRequest();
}
- async checkPendingImports() {
- try {
- const { bulkImportSourceGroups } = this.client.readQuery({
- query: bulkImportSourceGroupsQuery,
- });
-
- const groupsInProgress = bulkImportSourceGroups.nodes.filter(
- (g) => g.status === STATUSES.STARTED,
- );
- if (groupsInProgress.length) {
- const { data: results } = await this.client.query({
- query: generateGroupsQuery(groupsInProgress),
- fetchPolicy: 'no-cache',
- });
- const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)]));
- completedGroups.forEach((group) => {
- this.groupManager.setImportStatus(group, STATUSES.FINISHED);
- });
+ async updateImportsStatuses(importStatuses) {
+ importStatuses.forEach(({ id, status_name: statusName }) => {
+ const group = this.groupManager.findByImportId(id);
+ if (group.id) {
+ this.groupManager.setImportStatus(group, statusName);
}
- } catch (e) {
- createFlash({
- message: s__('BulkImport|Update of import statuses with realtime changes failed'),
- });
- } finally {
- this.timeoutId = setTimeout(() => this.checkPendingImports(), this.interval);
- }
+ });
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index cd646befaaa..cd837a840e4 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -14,6 +14,7 @@ export function mountImportGroupsApp(mountElement) {
statusPath,
availableNamespacesPath,
createBulkImportPath,
+ jobsPath,
sourceUrl,
} = mountElement.dataset;
const apolloProvider = new VueApollo({
@@ -22,6 +23,7 @@ export function mountImportGroupsApp(mountElement) {
status: statusPath,
availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
+ jobs: jobsPath,
},
}),
});
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 7107380b146..91ab68d5f39 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -4,7 +4,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-import { polyfillSticky } from '~/lib/utils/sticky';
import { sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import delayedJobMixin from '../mixins/delayed_job_mixin';
@@ -135,14 +134,6 @@ export default {
this.fetchJobsForStage(defaultStage);
}
}
-
- if (newVal.archived) {
- this.$nextTick(() => {
- if (this.$refs.sticky) {
- polyfillSticky(this.$refs.sticky);
- }
- });
- }
},
},
created() {
@@ -265,7 +256,6 @@ export default {
<div
v-if="job.archived"
- ref="sticky"
class="gl-mt-3 archived-job"
:class="{ 'sticky-top border-bottom-0': hasTrace }"
data-testid="archived-job"
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 2453fea9e58..fbdbfddff56 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -2,7 +2,6 @@
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { polyfillSticky } from '~/lib/utils/sticky';
import { __, sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -54,9 +53,6 @@ export default {
});
},
},
- mounted() {
- polyfillSticky(this.$el);
- },
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 6bb7f09b886..a6d53358cb8 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,5 +1,3 @@
-import StickyFill from 'stickyfilljs';
-
export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
@@ -60,13 +58,3 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
},
);
};
-
-/**
- * Polyfill the `position: sticky` behavior.
- *
- * - If the current environment supports `position: sticky`, do nothing.
- * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
- */
-export const polyfillSticky = (el) => {
- StickyFill.add(el);
-};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 6aa45ecc7a0..251f1e0515a 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -19,7 +19,6 @@ import {
} from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
-import { polyfillSticky } from './lib/utils/sticky';
import { getLocationHash } from './lib/utils/url_utility';
import { __ } from './locale';
import Notes from './notes';
@@ -123,7 +122,6 @@ export default class MergeRequestTabs {
) {
this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
}
- this.initAffix();
}
bindEvents() {
@@ -509,21 +507,4 @@ export default class MergeRequestTabs {
}
}, 0);
}
-
- initAffix() {
- const $tabs = $('.js-tabs-affix');
-
- // Screen space on small screens is usually very sparse
- // So we dont affix the tabs on these
- if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- /**
- If the browser does not support position sticky, it returns the position as static.
- If the browser does support sticky, then we allow the browser to handle it, if not
- then we default back to Bootstraps affix
- */
- if ($tabs.css('position') !== 'static') return;
-
- polyfillSticky($tabs);
- }
}
diff --git a/app/assets/javascripts/pages/import/bulk_imports/index.js b/app/assets/javascripts/pages/import/bulk_imports/status/index.js
index 37ac1a98466..37ac1a98466 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/index.js
+++ b/app/assets/javascripts/pages/import/bulk_imports/status/index.js
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1383f224979..f56d8f2c2a9 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -497,7 +497,7 @@
li {
a,
button,
- .dropdown-item {
+ .dropdown-item:not(.open-with-link) {
padding: 8px 40px;
position: relative;
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index 61eb9a27560..ef32ba4d119 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -37,9 +37,8 @@ class Import::BulkImportsController < ApplicationController
end
def create
- BulkImportService.new(current_user, create_params, credentials).execute
-
- render json: :ok
+ result = BulkImportService.new(current_user, create_params, credentials).execute
+ render json: result.to_json(only: [:id])
end
def realtime_changes
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c30fc0f5a73..c9e9a34ad88 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -36,7 +36,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
- push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
+ push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)
@@ -502,7 +502,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
params = request.query_parameters
params[:view] = "inline"
- if Feature.enabled?(:default_merge_ref_for_diffs, project)
+ if Feature.enabled?(:default_merge_ref_for_diffs, project, default_enabled: :yaml)
params = params.merge(diff_head: true)
end
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 3cf0a23b7f6..9ad700404ff 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -78,6 +78,8 @@ module Repositories
def update_fetch_statistics
return unless project
return if Gitlab::Database.read_only?
+ return if Feature.enabled?(:disable_git_http_fetch_writes)
+
return unless repo_type.project?
OnboardingProgressService.new(project.namespace).execute(action: :git_read)
diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb
index 4ade3e6f031..4408c9cdb6d 100644
--- a/app/finders/ci/jobs_finder.rb
+++ b/app/finders/ci/jobs_finder.rb
@@ -45,7 +45,8 @@ module Ci
return unless pipeline
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_build, pipeline)
- jobs_by_type(pipeline, type).latest
+ jobs_scope = jobs_by_type(pipeline, type)
+ params[:include_retried] ? jobs_scope : jobs_scope.latest
end
def filter_by_scope(builds)
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index bdcf7da3bea..89a28d9dfb8 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# WARNING: This finder does not check permissions!
+#
# Arguments:
# params:
# project: Project model - Find deployments for this project
@@ -27,11 +29,13 @@ class DeploymentsFinder
def execute
items = init_collection
items = by_updated_at(items)
+ items = by_finished_at(items)
items = by_environment(items)
items = by_status(items)
items = preload_associations(items)
- items = by_finished_between(items)
- sort(items)
+ items = sort(items)
+
+ items
end
private
@@ -44,11 +48,9 @@ class DeploymentsFinder
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def sort(items)
- items.order(sort_params)
+ items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord
end
- # rubocop: enable CodeReuse/ActiveRecord
def by_updated_at(items)
items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
@@ -57,6 +59,13 @@ class DeploymentsFinder
items
end
+ def by_finished_at(items)
+ items = items.finished_before(params[:finished_before]) if params[:finished_before].present?
+ items = items.finished_after(params[:finished_after]) if params[:finished_after].present?
+
+ items
+ end
+
def by_environment(items)
if params[:environment].present?
items.for_environment_name(params[:environment])
@@ -65,12 +74,6 @@ class DeploymentsFinder
end
end
- def by_finished_between(items)
- items = items.finished_between(params[:finished_after], params[:finished_before].presence) if params[:finished_after].present?
-
- items
- end
-
def by_status(items)
return items unless params[:status].present?
diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb
index 4721ebab41b..6a94d2f37b2 100644
--- a/app/graphql/mutations/merge_requests/update.rb
+++ b/app/graphql/mutations/merge_requests/update.rb
@@ -19,9 +19,14 @@ module Mutations
required: false,
description: copy_field_description(Types::MergeRequestType, :description)
- def resolve(args)
- merge_request = authorized_find!(**args.slice(:project_path, :iid))
- attributes = args.slice(:title, :description, :target_branch).compact
+ argument :state, ::Types::MergeRequestStateEventEnum,
+ required: false,
+ as: :state_event,
+ description: 'The action to perform to change the state.'
+
+ def resolve(project_path:, iid:, **args)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ attributes = args.compact
::MergeRequests::UpdateService
.new(merge_request.project, current_user, attributes)
diff --git a/app/graphql/types/merge_request_state_event_enum.rb b/app/graphql/types/merge_request_state_event_enum.rb
new file mode 100644
index 00000000000..ebb8b9638db
--- /dev/null
+++ b/app/graphql/types/merge_request_state_event_enum.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ class MergeRequestStateEventEnum < BaseEnum
+ graphql_name 'MergeRequestNewState'
+ description 'New state to apply to a merge request.'
+
+ value 'OPEN',
+ value: 'reopen',
+ description: 'Open the merge request if it is closed.'
+
+ value 'CLOSED',
+ value: 'close',
+ description: 'Close the merge request if it is open.'
+ end
+end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 312d535a92c..cfc4075100b 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -80,27 +80,27 @@ module LabelsHelper
def suggested_colors
{
- '#0033CC' => s_('SuggestedColors|UA blue'),
- '#428BCA' => s_('SuggestedColors|Moderate blue'),
- '#44AD8E' => s_('SuggestedColors|Lime green'),
- '#A8D695' => s_('SuggestedColors|Feijoa'),
- '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'),
- '#69D100' => s_('SuggestedColors|Bright green'),
- '#004E00' => s_('SuggestedColors|Very dark lime green'),
- '#34495E' => s_('SuggestedColors|Very dark desaturated blue'),
- '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'),
- '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'),
- '#5843AD' => s_('SuggestedColors|Dark moderate blue'),
- '#8E44AD' => s_('SuggestedColors|Dark moderate violet'),
- '#FFECDB' => s_('SuggestedColors|Very pale orange'),
- '#AD4363' => s_('SuggestedColors|Dark moderate pink'),
- '#D10069' => s_('SuggestedColors|Strong pink'),
- '#CC0033' => s_('SuggestedColors|Strong red'),
- '#FF0000' => s_('SuggestedColors|Pure red'),
- '#D9534F' => s_('SuggestedColors|Soft red'),
- '#D1D100' => s_('SuggestedColors|Strong yellow'),
- '#F0AD4E' => s_('SuggestedColors|Soft orange'),
- '#AD8D43' => s_('SuggestedColors|Dark moderate orange')
+ '#009966' => s_('SuggestedColors|Green-cyan'),
+ '#8fbc8f' => s_('SuggestedColors|Dark sea green'),
+ '#3cb371' => s_('SuggestedColors|Medium sea green'),
+ '#00b140' => s_('SuggestedColors|Green screen'),
+ '#013220' => s_('SuggestedColors|Dark green'),
+ '#6699cc' => s_('SuggestedColors|Blue-gray'),
+ '#0000ff' => s_('SuggestedColors|Blue'),
+ '#e6e6fa' => s_('SuggestedColors|Lavendar'),
+ '#9400d3' => s_('SuggestedColors|Dark violet'),
+ '#330066' => s_('SuggestedColors|Deep violet'),
+ '#808080' => s_('SuggestedColors|Gray'),
+ '#36454f' => s_('SuggestedColors|Charcoal grey'),
+ '#f7e7ce' => s_('SuggestedColors|Champagne'),
+ '#c21e56' => s_('SuggestedColors|Rose red'),
+ '#cc338b' => s_('SuggestedColors|Magenta-pink'),
+ '#dc143c' => s_('SuggestedColors|Crimson'),
+ '#ff0000' => s_('SuggestedColors|Red'),
+ '#cd5b45' => s_('SuggestedColors|Dark coral'),
+ '#eee600' => s_('SuggestedColors|Titanium yellow'),
+ '#ed9121' => s_('SuggestedColors|Carrot orange'),
+ '#c39953' => s_('SuggestedColors|Aztec Gold')
}
end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 5c9561ffa98..b260822f784 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -8,6 +8,7 @@ module Clusters
self.table_name = 'cluster_agent_tokens'
belongs_to :agent, class_name: 'Clusters::Agent'
+ belongs_to :created_by_user, class_name: 'User', optional: true
before_save :ensure_token
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2f0fd0af63b..ea2f425c5f6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -209,14 +209,26 @@ class CommitStatus < ApplicationRecord
end
def group_name
- # 'rspec:linux: 1/10' => 'rspec:linux'
- common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
+ simplified_commit_status_group_name_feature_flag = Gitlab::SafeRequestStore.fetch("project:#{project_id}:simplified_commit_status_group_name") do
+ Feature.enabled?(:simplified_commit_status_group_name, project, default_enabled: false)
+ end
+
+ if simplified_commit_status_group_name_feature_flag
+ # Only remove one or more [...] "X/Y" "X Y" from the end of build names.
+ # More about the regular expression logic: https://docs.gitlab.com/ee/ci/jobs/#group-jobs-in-a-pipeline
- # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
- common_name.gsub!(%r{: \[.*\]\s*\z}, '')
+ name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip
+ else
+ # Prior implementation, remove [...] "X/Y" "X Y" from the beginning and middle of build names
+ # 'rspec:linux: 1/10' => 'rspec:linux'
+ common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
- common_name.strip!
- common_name
+ # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
+ common_name.gsub!(%r{: \[.*\]\s*\z}, '')
+
+ common_name.strip!
+ common_name
+ end
end
def failed_but_allowed?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 7bcf7c702f6..f000e474605 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -38,6 +38,7 @@ class Deployment < ApplicationRecord
scope :for_status, -> (status) { where(status: status) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
+ scope :for_projects, -> (projects) { where(project: projects) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
@@ -45,11 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
- scope :finished_between, -> (start_date, end_date = nil) do
- selected = where('deployments.finished_at >= ?', start_date)
- selected = selected.where('deployments.finished_at < ?', end_date) if end_date
- selected
- end
+ scope :finished_after, ->(date) { where('finished_at >= ?', date) }
+ scope :finished_before, ->(date) { where('finished_at < ?', date) }
FINISHED_STATUSES = %i[success failed canceled].freeze
diff --git a/app/models/label.rb b/app/models/label.rb
index 54129c7c7f3..7a31b095cfc 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -12,7 +12,7 @@ class Label < ApplicationRecord
cache_markdown_field :description, pipeline: :single_line
- DEFAULT_COLOR = '#428BCA'
+ DEFAULT_COLOR = '#6699cc'
default_value_for :color, DEFAULT_COLOR
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5fad876d3fb..1374e8a814a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -921,6 +921,10 @@ class MergeRequest < ApplicationRecord
closed? && !source_project_missing? && source_branch_exists?
end
+ def can_be_closed?
+ opened?
+ end
+
def ensure_merge_request_diff
merge_request_diff.persisted? || create_merge_request_diff
end
diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb
index bebf9153ce7..29439a79afe 100644
--- a/app/services/bulk_import_service.rb
+++ b/app/services/bulk_import_service.rb
@@ -38,6 +38,8 @@ class BulkImportService
bulk_import = create_bulk_import
BulkImportWorker.perform_async(bulk_import.id)
+
+ bulk_import
end
private
diff --git a/app/services/merge_requests/reload_merge_head_diff_service.rb b/app/services/merge_requests/reload_merge_head_diff_service.rb
index 66fcb5c022b..f02a9bd3139 100644
--- a/app/services/merge_requests/reload_merge_head_diff_service.rb
+++ b/app/services/merge_requests/reload_merge_head_diff_service.rb
@@ -24,7 +24,7 @@ module MergeRequests
attr_reader :merge_request
def enabled?
- Feature.enabled?(:default_merge_ref_for_diffs, merge_request.project)
+ Feature.enabled?(:default_merge_ref_for_diffs, merge_request.project, default_enabled: :yaml)
end
def recreate_merge_head_diff
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index 9ae71eabc8e..778bc1ef1a4 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -8,4 +8,5 @@
#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
available_namespaces_path: import_available_namespaces_path(format: :json),
create_bulk_import_path: import_bulk_imports_path(format: :json),
+ jobs_path: realtime_changes_import_bulk_imports_path(format: :json),
source_url: @source_url } }
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 938dfc69500..0ec47744fc9 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -6,9 +6,9 @@
%span.gl-mr-2.js-clone-dropdown-label
= _('Clone')
= sprite_icon("chevron-down", css_class: "icon")
- %ul.p-3.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class }
+ %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class }
- if ssh_enabled?
- %li
+ %li{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with SSH')
.input-group
@@ -17,7 +17,7 @@
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
- if http_enabled?
- %li.pt-2
+ %li.pt-2{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group
@@ -25,4 +25,15 @@
.input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
+ %li.divider.mt-2
+ %li.pt-2.gl-new-dropdown-item
+ %label.label-bold{ class: 'gl-px-4!' }
+ = _('Open in your IDE')
+ %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + CGI.escape(project.http_url_to_repo) }
+ .gl-new-dropdown-item-text-wrapper
+ = _('Visual Studio Code')
+ - if show_xcode_link?(@project)
+ %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
+ .gl-new-dropdown-item-text-wrapper
+ = _("Xcode")
= render_if_exists 'projects/buttons/kerberos_clone_field'
diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml
deleted file mode 100644
index e0f47f1ca3d..00000000000
--- a/app/views/projects/buttons/_xcode_link.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%a.gl-button.btn.btn-default{ href: xcode_uri_to_repo(@project) }
- = _("Open in Xcode")
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 2a502f6e613..453a34d1e7a 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -78,7 +78,7 @@
- if mr_action === "diffs"
- add_page_startup_api_call @endpoint_metadata_url
- params = request.query_parameters
- - if Feature.enabled?(:default_merge_ref_for_diffs, @project)
+ - if Feature.enabled?(:default_merge_ref_for_diffs, @project, default_enabled: :yaml)
- params = params.merge(diff_head: true)
= render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: { "is-locked": @merge_request.discussion_locked?,
endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', params),
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index cd6e85d60ed..6d33fbb535e 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -11,11 +11,6 @@
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
-
- - if show_xcode_link?(@project)
- .project-action-button.project-xcode<
- = render "projects/buttons/xcode_link"
-
= render 'projects/buttons/download', project: @project, ref: @ref
.project-clone-holder.d-none.d-md-inline-block>
diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
index 14f4b04ef78..4b008601783 100644
--- a/app/views/shared/notifications/_new_button.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -17,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
- %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
+ %button.btn.gl-button.btn-default.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("chevron-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("chevron-down", css_class: "icon")
diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb
index f78564cc69d..5d395c9e38a 100644
--- a/app/workers/pages_transfer_worker.rb
+++ b/app/workers/pages_transfer_worker.rb
@@ -9,7 +9,7 @@ class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker
loggable_arguments 0, 1
def perform(method, args)
- return unless Gitlab::PagesTransfer::Async::METHODS.include?(method)
+ return unless Gitlab::PagesTransfer::METHODS.include?(method)
result = Gitlab::PagesTransfer.new.public_send(method, *args) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/changelogs/unreleased/220957-Open-with-IDE.yml b/changelogs/unreleased/220957-Open-with-IDE.yml
new file mode 100644
index 00000000000..c37adfed6b1
--- /dev/null
+++ b/changelogs/unreleased/220957-Open-with-IDE.yml
@@ -0,0 +1,5 @@
+---
+title: Allow opening projects with VS Code
+merge_request: 49460
+author: Kev @KevSlashNull
+type: added
diff --git a/changelogs/unreleased/273067-fix-ide-open-mr-highligh-first-file.yml b/changelogs/unreleased/273067-fix-ide-open-mr-highligh-first-file.yml
new file mode 100644
index 00000000000..27928522b76
--- /dev/null
+++ b/changelogs/unreleased/273067-fix-ide-open-mr-highligh-first-file.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Web IDE open MR to show opened files consistently
+merge_request: 53927
+author:
+type: fixed
diff --git a/changelogs/unreleased/276917-remove-default_merge_ref_for_diffs-feature-flag.yml b/changelogs/unreleased/276917-remove-default_merge_ref_for_diffs-feature-flag.yml
new file mode 100644
index 00000000000..69287c045eb
--- /dev/null
+++ b/changelogs/unreleased/276917-remove-default_merge_ref_for_diffs-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Make merge-ref base the default comparison mode
+merge_request: 54017
+author:
+type: added
diff --git a/changelogs/unreleased/293720-token-created-by.yml b/changelogs/unreleased/293720-token-created-by.yml
new file mode 100644
index 00000000000..efbefa9e169
--- /dev/null
+++ b/changelogs/unreleased/293720-token-created-by.yml
@@ -0,0 +1,5 @@
+---
+title: Add created_by_user to cluster agent tokens
+merge_request: 54019
+author:
+type: added
diff --git a/changelogs/unreleased/298801-group-migration-show-import-status-frontend.yml b/changelogs/unreleased/298801-group-migration-show-import-status-frontend.yml
new file mode 100644
index 00000000000..e65b975199b
--- /dev/null
+++ b/changelogs/unreleased/298801-group-migration-show-import-status-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Use realtime_changes endpoint for reporting group import status
+merge_request: 52796
+author:
+type: changed
diff --git a/changelogs/unreleased/ApplyGitLabUIstylesin_notifications_directory.yml b/changelogs/unreleased/ApplyGitLabUIstylesin_notifications_directory.yml
new file mode 100644
index 00000000000..f66ea40f48b
--- /dev/null
+++ b/changelogs/unreleased/ApplyGitLabUIstylesin_notifications_directory.yml
@@ -0,0 +1,5 @@
+---
+title: Apply GitLab UI styles to buttons in notification directory _new_button.html.haml
+merge_request: 51148
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/ajk-add-state-events-to-mr-update-mutation.yml b/changelogs/unreleased/ajk-add-state-events-to-mr-update-mutation.yml
new file mode 100644
index 00000000000..c8539b73688
--- /dev/null
+++ b/changelogs/unreleased/ajk-add-state-events-to-mr-update-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: Add state events to merge request update mutation
+merge_request: 54133
+author:
+type: added
diff --git a/changelogs/unreleased/ar-color-name-changes.yml b/changelogs/unreleased/ar-color-name-changes.yml
new file mode 100644
index 00000000000..e10b9a0a6cd
--- /dev/null
+++ b/changelogs/unreleased/ar-color-name-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Update the HEX values and names of the color options for labels
+merge_request: 50393
+author:
+type: changed
diff --git a/changelogs/unreleased/leipert-remove-sticky-polyfill.yml b/changelogs/unreleased/leipert-remove-sticky-polyfill.yml
new file mode 100644
index 00000000000..1deb36df8ea
--- /dev/null
+++ b/changelogs/unreleased/leipert-remove-sticky-polyfill.yml
@@ -0,0 +1,5 @@
+---
+title: Remove position sticky polyfill
+merge_request: 54299
+author:
+type: changed
diff --git a/changelogs/unreleased/return-all-jobs.yml b/changelogs/unreleased/return-all-jobs.yml
new file mode 100644
index 00000000000..3e19cd888d1
--- /dev/null
+++ b/changelogs/unreleased/return-all-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to retrieve all jobs for a given pipeline
+merge_request: 48589
+author: Alexander Kutelev
+type: fixed
diff --git a/config/feature_flags/development/default_merge_ref_for_diffs.yml b/config/feature_flags/development/default_merge_ref_for_diffs.yml
index 1058cff5e8f..f197044d47f 100644
--- a/config/feature_flags/development/default_merge_ref_for_diffs.yml
+++ b/config/feature_flags/development/default_merge_ref_for_diffs.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276917
milestone: '13.4'
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/disable_git_http_fetch_writes.yml b/config/feature_flags/development/disable_git_http_fetch_writes.yml
new file mode 100644
index 00000000000..aba9df97d95
--- /dev/null
+++ b/config/feature_flags/development/disable_git_http_fetch_writes.yml
@@ -0,0 +1,8 @@
+---
+name: disable_git_http_fetch_writes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54322
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/883
+milestone: '13.9'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/disable_ssh_key_used_tracking.yml b/config/feature_flags/development/disable_ssh_key_used_tracking.yml
new file mode 100644
index 00000000000..2efd38b073a
--- /dev/null
+++ b/config/feature_flags/development/disable_ssh_key_used_tracking.yml
@@ -0,0 +1,8 @@
+---
+name: disable_ssh_key_used_tracking
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54335
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/889
+milestone: '13.9'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/gitlab_ci_archived_trace_consistent_reads.yml b/config/feature_flags/development/gitlab_ci_archived_trace_consistent_reads.yml
new file mode 100644
index 00000000000..c2a64263f08
--- /dev/null
+++ b/config/feature_flags/development/gitlab_ci_archived_trace_consistent_reads.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_ci_archived_trace_consistent_reads
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53716
+rollout_issue_url:
+milestone: '13.9'
+type: development
+group: group::continuous integration
+default_enabled: false
diff --git a/config/feature_flags/development/simplified_commit_status_group_name.yml b/config/feature_flags/development/simplified_commit_status_group_name.yml
new file mode 100644
index 00000000000..410d351de5f
--- /dev/null
+++ b/config/feature_flags/development/simplified_commit_status_group_name.yml
@@ -0,0 +1,8 @@
+---
+name: simplified_commit_status_group_name
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52644
+rollout_issue_url:
+milestone: '13.9'
+type: development
+group: group::testing
+default_enabled: false
diff --git a/db/migrate/20210211195543_add_created_by_user_for_cluster_agent_token.rb b/db/migrate/20210211195543_add_created_by_user_for_cluster_agent_token.rb
new file mode 100644
index 00000000000..94dc8192037
--- /dev/null
+++ b/db/migrate/20210211195543_add_created_by_user_for_cluster_agent_token.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddCreatedByUserForClusterAgentToken < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_cluster_agent_tokens_on_created_by_user_id'
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:cluster_agent_tokens, :created_by_user_id)
+ add_column :cluster_agent_tokens, :created_by_user_id, :bigint
+ end
+
+ add_concurrent_index :cluster_agent_tokens, :created_by_user_id, name: INDEX_NAME
+ add_concurrent_foreign_key :cluster_agent_tokens, :users, column: :created_by_user_id, on_delete: :nullify
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :cluster_agent_tokens, :users, column: :created_by_user_id
+ end
+
+ remove_concurrent_index_by_name :cluster_agent_tokens, INDEX_NAME
+ remove_column :cluster_agent_tokens, :created_by_user_id
+ end
+end
diff --git a/db/schema_migrations/20210211195543 b/db/schema_migrations/20210211195543
new file mode 100644
index 00000000000..7f15ca21d36
--- /dev/null
+++ b/db/schema_migrations/20210211195543
@@ -0,0 +1 @@
+484338ddc83bfb44523d08da92ac7f5b9d13e1a66ad1c9c3f7590f91fc9305c0 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 045b3000998..429f233c8d5 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11024,6 +11024,7 @@ CREATE TABLE cluster_agent_tokens (
updated_at timestamp with time zone NOT NULL,
agent_id bigint NOT NULL,
token_encrypted text NOT NULL,
+ created_by_user_id bigint,
CONSTRAINT check_c60daed227 CHECK ((char_length(token_encrypted) <= 255))
);
@@ -21843,6 +21844,8 @@ CREATE UNIQUE INDEX index_ci_variables_on_project_id_and_key_and_environment_sco
CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USING btree (agent_id);
+CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id);
+
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
CREATE UNIQUE INDEX index_cluster_agents_on_project_id_and_name ON cluster_agents USING btree (project_id, name);
@@ -24369,6 +24372,9 @@ ALTER TABLE ONLY vulnerabilities
ALTER TABLE ONLY index_statuses
ADD CONSTRAINT fk_74b2492545 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY cluster_agent_tokens
+ ADD CONSTRAINT fk_75008f3553 FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_76bc5f5455 FOREIGN KEY (resolved_by_id) REFERENCES users(id) ON DELETE SET NULL;
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index d269d28d604..655e35c3e60 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -234,6 +234,7 @@ control over how the Pages daemon runs and serves content in your environment.
| `domain_config_source` | Domain configuration source (default: `auto`)
| `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab.
| `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab.
+| `auth_scope` | The OAuth application scope to use for authentication. Must match GitLab Pages OAuth application settings. Leave blank to use `api` scope by default.
| `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`.
| `headers` | Specify any additional http headers that should be sent to the client with each response.
| `inplace_chroot` | On [systems that don't support bind-mounts](index.md#additional-configuration-for-docker-container), this instructs GitLab Pages to `chroot` into its `pages_path` directory. Some caveats exist when using in-place `chroot`; refer to the GitLab Pages [README](https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md#caveats) for more information.
@@ -400,6 +401,25 @@ NOTE:
For this setting to be effective with multi-node setups, it has to be applied to
all the App nodes and Sidekiq nodes.
+#### Using Pages with reduced authentication scope
+
+By default, the Pages daemon uses the `api` scope to authenticate. You can configure this. For
+example, this reduces the scope to `read_api` in `/etc/gitlab/gitlab.rb`:
+
+```ruby
+gitlab_pages['auth_scope'] = 'read_api'
+```
+
+The scope to use for authentication must match the GitLab Pages OAuth application settings. Users of
+pre-existing applications must modify the GitLab Pages OAuth application. Follow these steps to do
+this:
+
+1. Navigate to your instance's **Admin Area > Settings > Applications** and expand **GitLab Pages**
+ settings.
+1. Clear the `api` scope's checkbox and select the desired scope's checkbox (for example,
+ `read_api`).
+1. Click **Save changes**.
+
#### Disabling public access to all Pages websites
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32095) in GitLab 12.7.
diff --git a/doc/api/dora4_group_analytics.md b/doc/api/dora4_group_analytics.md
new file mode 100644
index 00000000000..7a7260f40e8
--- /dev/null
+++ b/doc/api/dora4_group_analytics.md
@@ -0,0 +1,84 @@
+---
+stage: Release
+group: Release
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+type: reference, api
+---
+
+# DORA4 Analytics Group API **(ULTIMATE ONLY)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab 13.9.
+> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-dora4-analytics-group-api). **(ULTIMATE ONLY)**
+
+WARNING:
+This feature might not be available to you. Check the **version history** note above for details.
+
+All methods require reporter authorization.
+
+## List group deployment frequencies
+
+Get a list of all group deployment frequencies:
+
+```plaintext
+GET /groups/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
+```
+
+Attributes:
+
+| Attribute | Type | Required | Description |
+|--------------|--------|----------|-----------------------|
+| `id` | string | yes | The ID of the group. |
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+|--------------|--------|----------|-----------------------|
+| `environment`| string | yes | The name of the environment to filter by. |
+| `from` | string | yes | Datetime range to start from. Inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). |
+| `to` | string | no | Datetime range to end at. Exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). |
+| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`). |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval"
+```
+
+Example response:
+
+```json
+[
+ {
+ "from": "2017-01-01",
+ "to": "2017-01-02",
+ "value": 106
+ },
+ {
+ "from": "2017-01-02",
+ "to": "2017-01-03",
+ "value": 55
+ }
+]
+```
+
+## Enable or disable DORA4 Analytics Group API **(ULTIMATE ONLY)**
+
+DORA4 Analytics Group API is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:dora4_group_deployment_frequency_api)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:dora4_group_deployment_frequency_api)
+```
diff --git a/doc/api/dora4_project_analytics.md b/doc/api/dora4_project_analytics.md
new file mode 100644
index 00000000000..a1becb056bf
--- /dev/null
+++ b/doc/api/dora4_project_analytics.md
@@ -0,0 +1,54 @@
+---
+stage: Release
+group: Release
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+type: reference, api
+---
+
+# DORA4 Analytics Project API **(ULTIMATE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.7.
+
+All methods require reporter authorization.
+
+## List project deployment frequencies
+
+Get a list of all project deployment frequencies, sorted by date:
+
+```plaintext
+GET /projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
+```
+
+| Attribute | Type | Required | Description |
+|--------------|--------|----------|-----------------------|
+| `id` | string | yes | The ID of the project |
+
+| Parameter | Type | Required | Description |
+|--------------|--------|----------|-----------------------|
+| `environment`| string | yes | The name of the environment to filter by |
+| `from` | string | yes | Datetime range to start from, inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
+| `to` | string | no | Datetime range to end at, exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
+| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`) |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval"
+```
+
+Example response:
+
+```json
+[
+ {
+ "from": "2017-01-01",
+ "to": "2017-01-02",
+ "value": 106
+ },
+ {
+ "from": "2017-01-02",
+ "to": "2017-01-03",
+ "value": 55
+ }
+]
+```
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index b7fdf52dca0..ef85292a17d 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -3522,6 +3522,11 @@ type ClusterAgentToken {
createdAt: Time
"""
+ The user who created the token.
+ """
+ createdByUser: User
+
+ """
Global ID of the token.
"""
id: ClustersAgentTokenID!
@@ -14840,7 +14845,7 @@ input LabelCreateInput {
(e.g. #FFAABB) or one of the CSS color names in
https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords.
"""
- color: String = "#428BCA"
+ color: String = "#6699cc"
"""
Description of the label.
@@ -15918,6 +15923,21 @@ Identifier of MergeRequest.
scalar MergeRequestID
"""
+New state to apply to a merge request.
+"""
+enum MergeRequestNewState {
+ """
+ Close the merge request if it is open.
+ """
+ CLOSED
+
+ """
+ Open the merge request if it is closed.
+ """
+ OPEN
+}
+
+"""
Check permissions for the current user on a merge request
"""
type MergeRequestPermissions {
@@ -16417,6 +16437,11 @@ input MergeRequestUpdateInput {
projectPath: ID!
"""
+ The action to perform to change the state.
+ """
+ state: MergeRequestNewState
+
+ """
Target branch of the merge request.
"""
targetBranch: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 0bf3042044c..0537007f665 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -9548,6 +9548,20 @@
"deprecationReason": null
},
{
+ "name": "createdByUser",
+ "description": "The user who created the token.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "id",
"description": "Global ID of the token.",
"args": [
@@ -40547,7 +40561,7 @@
"name": "String",
"ofType": null
},
- "defaultValue": "\"#428BCA\""
+ "defaultValue": "\"#6699cc\""
},
{
"name": "clientMutationId",
@@ -43514,6 +43528,29 @@
"possibleTypes": null
},
{
+ "kind": "ENUM",
+ "name": "MergeRequestNewState",
+ "description": "New state to apply to a merge request.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "OPEN",
+ "description": "Open the merge request if it is closed.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CLOSED",
+ "description": "Close the merge request if it is open.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "MergeRequestPermissions",
"description": "Check permissions for the current user on a merge request",
@@ -44843,6 +44880,16 @@
"defaultValue": null
},
{
+ "name": "state",
+ "description": "The action to perform to change the state.",
+ "type": {
+ "kind": "ENUM",
+ "name": "MergeRequestNewState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9c2d28ed743..55bf9e5d03c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -557,6 +557,7 @@ Autogenerated return type of ClusterAgentDelete.
| ----- | ---- | ----------- |
| `clusterAgent` | ClusterAgent | Cluster agent this token is associated with. |
| `createdAt` | Time | Timestamp the token was created. |
+| `createdByUser` | User | The user who created the token. |
| `id` | ClustersAgentTokenID! | Global ID of the token. |
### ClusterAgentTokenCreatePayload
@@ -5111,6 +5112,15 @@ Possible identifier types for a measurement.
| `PROJECTS` | Project count |
| `USERS` | User count |
+### MergeRequestNewState
+
+New state to apply to a merge request..
+
+| Value | Description |
+| ----- | ----------- |
+| `CLOSED` | Close the merge request if it is open. |
+| `OPEN` | Open the merge request if it is closed. |
+
### MergeRequestSort
Values for sorting merge requests.
diff --git a/doc/api/group_import_export.md b/doc/api/group_import_export.md
index 6def5075c22..fca62627200 100644
--- a/doc/api/group_import_export.md
+++ b/doc/api/group_import_export.md
@@ -20,6 +20,7 @@ Group exports include the following:
- Group badges
- Group members
- Sub-groups. Each sub-group includes all data above
+- Group wikis **(PREMIUM SELF)**
## Schedule new export
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 2c935b6e288..18d2e5ec3df 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -154,11 +154,12 @@ Get a list of jobs for a pipeline.
GET /projects/:id/pipelines/:pipeline_id/jobs
```
-| Attribute | Type | Required | Description |
-|---------------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
-| `pipeline_id` | integer | yes | ID of a pipeline. |
-| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
+| Attribute | Type | Required | Description |
+|-------------------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `pipeline_id` | integer | yes | ID of a pipeline. |
+| `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. |
+| `include_retried` | boolean | no | Include retried jobs in the response. Defaults to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/272627) in GitLab 13.9. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running"
@@ -260,7 +261,6 @@ Example of response
"status": "pending"
},
"ref": "master",
- "artifacts": [],
"runner": null,
"stage": "test",
"status": "failed",
@@ -290,6 +290,12 @@ Example of response
In GitLab 13.3 and later, this endpoint [returns data for any pipeline](pipelines.md#single-pipeline-requests)
including [child pipelines](../ci/parent_child_pipelines.md).
+In GitLab 13.5 and later, this endpoint does not return retried jobs in the response
+by default.
+
+In GitLab 13.9 and later, this endpoint can include retried jobs in the response
+with `include_retried` set to `true`.
+
## List pipeline bridges
Get a list of bridge jobs for a pipeline.
diff --git a/doc/api/project_analytics.md b/doc/api/project_analytics.md
index 8f1ba912e57..d89c173dd3e 100644
--- a/doc/api/project_analytics.md
+++ b/doc/api/project_analytics.md
@@ -1,52 +1,8 @@
---
-stage: Release
-group: Release
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
-type: reference, api
+redirect_to: 'dora4_project_analytics.md'
---
-# Project Analytics API **(ULTIMATE SELF)**
+This document was moved to [another location](dora4_project_analytics.md).
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.7.
-
-All methods require reporter authorization.
-
-## List project deployment frequencies
-
-Get a list of all project aliases:
-
-```plaintext
-GET /projects/:id/analytics/deployment_frequency?environment=:environment&from=:from&to=:to&interval=:interval
-```
-
-| Attribute | Type | Required | Description |
-|--------------|--------|----------|-----------------------|
-| `id` | string | yes | The ID of the project |
-
-| Parameter | Type | Required | Description |
-|--------------|--------|----------|-----------------------|
-| `environment`| string | yes | The name of the environment to filter by |
-| `from` | string | yes | Datetime range to start from, inclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
-| `to` | string | no | Datetime range to end at, exclusive, ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) |
-| `interval` | string | no | The bucketing interval (`all`, `monthly`, `daily`) |
-
-```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/analytics/deployment_frequency?from=:from&to=:to&interval=:interval"
-```
-
-Example response:
-
-```json
-[
- {
- "from": "2017-01-01",
- "to": "2017-01-02",
- "value": 106
- },
- {
- "from": "2017-01-02",
- "to": "2017-01-03",
- "value": 55
- }
-]
-```
+<!-- This redirect file can be deleted after <2021-04-25>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 08059ab3cde..953608fc1e8 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -90,7 +90,7 @@ GitLab CI/CD uses a number of concepts to describe and run your build and deploy
| Concept | Description |
|:--------------------------------------------------------|:-------------------------------------------------------------------------------|
| [Pipelines](pipelines/index.md) | Structure your CI/CD process through pipelines. |
-| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. |
+| [CI/CD variables](variables/README.md) | Reuse values based on a variable/value key pair. |
| [Environments](environments/index.md) | Deploy your application to different environments (e.g., staging, production). |
| [Job artifacts](pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
diff --git a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
index b7418f08bf5..38930eb60ad 100644
--- a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
@@ -50,7 +50,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
![Bitbucket Cloud webhook](img/bitbucket_app_password.png)
-1. In GitLab, from **Settings > CI/CD > Environment variables**, add variables to allow
+1. In GitLab, from **Settings > CI/CD > Variables**, add variables to allow
communication with Bitbucket via the Bitbucket API:
`BITBUCKET_ACCESS_TOKEN`: the Bitbucket app password created above.
diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md
index a4b45bfd29f..ccacb3c61d3 100644
--- a/doc/ci/cloud_deployment/index.md
+++ b/doc/ci/cloud_deployment/index.md
@@ -109,7 +109,7 @@ The ECS task definition can be:
After you have these prerequisites ready, follow these steps:
-1. Make sure your AWS credentials are set up as environment variables for your
+1. Make sure your AWS credentials are set up as CI/CD variables for your
project. You can follow [the steps above](#run-aws-commands-from-gitlab-cicd) to complete this setup.
1. Add these variables to your project's `.gitlab-ci.yml` file, or in the project's
[CI/CD settings](../variables/README.md#create-a-custom-variable-in-the-ui):
@@ -242,7 +242,7 @@ pass three JSON input objects, based on existing templates:
have two ways to pass in these JSON objects:
- They can be three actual files located in your project. You must specify their path relative to
- your project root in your `.gitlab-ci.yml` file, using the following variables. For example, if
+ your project root in your `.gitlab-ci.yml` file, using the following CI/CD variables. For example, if
your files are in a `<project_root>/aws` folder:
```yaml
@@ -286,7 +286,7 @@ When running your project pipeline at this point:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6.
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
-AWS EC2, first you must define [your AWS credentials as environment variables](#run-aws-commands-from-gitlab-cicd).
+AWS EC2, first you must define [your AWS credentials as CI/CD variables](#run-aws-commands-from-gitlab-cicd).
Next, define a job for the `build` stage. To do so, you must reference the
`Auto-DevOps.gitlab-ci.yml` template and include a job named `build_artifact` in your
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 11020f0a090..46ced9b4d6d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -830,7 +830,7 @@ which can be avoided if a different driver is used, for example `overlay2`.
### Use the OverlayFS driver per project
You can enable the driver for each project individually by using the `DOCKER_DRIVER`
-environment [variable](../yaml/README.md#variables) in `.gitlab-ci.yml`:
+[CI/CD variable](../yaml/README.md#variables) in `.gitlab-ci.yml`:
```yaml
variables:
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 743808c09e1..67450d442a9 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -272,11 +272,11 @@ test:
- bundle exec rake spec
```
-## Passing environment variables to services
+## Passing CI/CD variables to services
-You can also pass custom environment [variables](../variables/README.md)
+You can also pass custom CI/CD [variables](../variables/README.md)
to fine tune your Docker `images` and `services` directly in the `.gitlab-ci.yml` file.
-For more information, read [custom environment variables](../variables/README.md#gitlab-ciyml-defined-variables)
+For more information, read about [`.gitlab-ci.yml` defined variables](../variables/README.md#gitlab-ciyml-defined-variables).
```yaml
# The following variables are automatically passed down to the Postgres container
@@ -528,7 +528,7 @@ To access private container registries, the GitLab Runner process can use:
To define which should be used, the GitLab Runner process reads the configuration in the following order:
- `DOCKER_AUTH_CONFIG` variable provided as either:
- - A [variable](../variables/README.md) in `.gitlab-ci.yml`.
+ - A [CI/CD variable](../variables/README.md) in `.gitlab-ci.yml`.
- A project's variables stored on the projects **Settings > CI/CD** page.
- `DOCKER_AUTH_CONFIG` variable provided as environment variable in `config.toml` of the runner.
- `config.json` file placed in `$HOME/.docker` directory of the user running GitLab Runner process.
@@ -627,7 +627,7 @@ Use one of the following methods to determine the value of `DOCKER_AUTH_CONFIG`:
To configure a single job with access for `registry.example.com:5000`,
follow these steps:
-1. Create a [variable](../variables/README.md) `DOCKER_AUTH_CONFIG` with the content of the
+1. Create a [CI/CD variable](../variables/README.md) `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
```json
@@ -702,7 +702,7 @@ To configure credentials store, follow these steps:
1. Make GitLab Runner use it. There are two ways to accomplish this. Either:
- Create a
- [variable](../variables/README.md)
+ [CI/CD variable](../variables/README.md)
`DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
@@ -734,7 +734,7 @@ To configure access for `aws_account_id.dkr.ecr.region.amazonaws.com`, follow th
Make sure that GitLab Runner can access the credentials.
1. Make GitLab Runner use it. There are two ways to accomplish this. Either:
- - Create a [variable](../variables/README.md)
+ - Create a [CI/CD variable](../variables/README.md)
`DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
@@ -781,13 +781,13 @@ registries to the `"credHelpers"` hash as described above.
Many services accept environment variables, which you can use to change
database names or set account names, depending on the environment.
-GitLab Runner 0.5.0 and up passes all YAML-defined variables to the created
+GitLab Runner 0.5.0 and up passes all YAML-defined CI/CD variables to the created
service containers.
For all possible configuration variables, check the documentation of each image
provided in their corresponding Docker hub page.
-All variables are passed to all services containers. It's not
+All CI/CD variables are passed to all services containers. It's not
designed to distinguish which variable should go where.
### PostgreSQL service example
diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md
index 9ae79ef40e8..eecc8ffd18f 100644
--- a/doc/ci/environments/deployment_safety.md
+++ b/doc/ci/environments/deployment_safety.md
@@ -110,7 +110,7 @@ for an explanation of these roles and the permissions of each.
Production secrets are needed to deploy successfully. For example, when deploying to the cloud,
cloud providers require these secrets to connect to their services. In the project settings, you can
-define and protect environment variables for these secrets. [Protected variables](../variables/README.md#protect-a-custom-variable)
+define and protect CI/CD variables for these secrets. [Protected variables](../variables/README.md#protect-a-custom-variable)
are only passed to pipelines running on [protected branches](../../user/project/protected_branches.md)
or [protected tags](../../user/project/protected_tags.md).
The other pipelines don't get the protected variable. You can also
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index ba73d48dd48..b49fcd72172 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -135,12 +135,12 @@ In summary, with the above `.gitlab-ci.yml` we have achieved the following:
job deploys our code to a staging server while the deployment
is recorded in an environment named `staging`.
-#### Environment variables and runners
+#### CI/CD variables and runners
Starting with GitLab 8.15, the environment name is exposed to the runner in
two forms:
-- `$CI_ENVIRONMENT_NAME`. The name given in `.gitlab-ci.yml` (with any variables
+- `$CI_ENVIRONMENT_NAME`. The name given in `.gitlab-ci.yml` (with any CI/CD variables
expanded).
- `$CI_ENVIRONMENT_SLUG`. A "cleaned-up" version of the name, suitable for use in URLs,
DNS, etc.
@@ -221,7 +221,7 @@ The assigned URL for the `review/your-branch-name` environment is [visible in th
Note the following:
- `stop_review` doesn't generate a dotenv report artifact, so it doesn't recognize the
- `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you shouldn't set `environment:url:` in the
+ `DYNAMIC_ENVIRONMENT_URL` environment variable. Therefore you shouldn't set `environment:url:` in the
`stop_review` job.
- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update
the environment URL.
@@ -327,7 +327,7 @@ For more information, see [Where variables can be used](../variables/where_varia
#### Example configuration
-Runners expose various [environment variables](../variables/README.md) when a job runs, so
+Runners expose various [predefined CI/CD variables](../variables/predefined_variables.md) when a job runs, so
you can use them as environment names.
In the following example, the job deploys to all branches except `master`:
@@ -825,7 +825,7 @@ build with the specified environment runs. Newer deployments can also
You may want to specify an environment keyword to
[protect builds from unauthorized access](protected_environments.md), or to get
-access to [scoped variables](#scoping-environments-with-specs). In these cases,
+access to [environment-scoped variables](#scoping-environments-with-specs). In these cases,
you can use the `action: prepare` keyword to ensure deployments aren't created,
and no builds are canceled:
@@ -846,7 +846,7 @@ build:
As documented in [Configuring dynamic environments](#configuring-dynamic-environments), you can
prepend environment name with a word, followed by a `/`, and finally the branch
-name, which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
+name, which is automatically defined by the `CI_COMMIT_REF_NAME` predefined CI/CD variable.
In short, environments that are named like `type/foo` are all presented under the same
group, named `type`.
@@ -1009,9 +1009,9 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
### Scoping environments with specs
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
-> - [Scoping for environment variables was moved to Free](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2.
+> - [Environment scoping for CI/CD variables was moved to all tiers](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2.
-You can limit the environment scope of a variable by
+You can limit the environment scope of a CI/CD variable by
defining which environments it can be available for.
Wildcards can be used and the default environment scope is `*`. This means that
@@ -1061,7 +1061,7 @@ environment's operational health.
## Limitations
-In the `environment: name`, you are limited to only the [predefined environment variables](../variables/predefined_variables.md).
+In the `environment: name`, you are limited to only the [predefined CI/CD variables](../variables/predefined_variables.md).
Re-using variables defined inside `script` as part of the environment name doesn't work.
## Further reading
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index fdebc1affc0..2d8c92a1a74 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -30,7 +30,7 @@ You must replace the `vault.example.com` URL below with the URL of your Vault se
## How it works
-Each job has JSON Web Token (JWT) provided as environment variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method.
+Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method.
The JWT's payload looks like this:
@@ -187,7 +187,7 @@ read_secrets:
- echo $CI_COMMIT_REF_NAME
# and is this ref protected
- echo $CI_COMMIT_REF_PROTECTED
- # Vault's address can be provided here or as CI variable
+ # Vault's address can be provided here or as CI/CD variable
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://www.vaultproject.io/api/auth/jwt#parameters-1
@@ -211,7 +211,7 @@ read_secrets:
- echo $CI_COMMIT_REF_NAME
# and is this ref protected
- echo $CI_COMMIT_REF_PROTECTED
- # Vault's address can be provided here or as CI variable
+ # Vault's address can be provided here or as CI/CD variable
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://www.vaultproject.io/api/auth/jwt#parameters-1
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index d9e99b3fb38..a02a5347734 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -120,7 +120,7 @@ cat ~/.ssh/id_rsa
```
Now, let's add it to your GitLab project as a [CI/CD variable](../../variables/README.md).
-Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
+Project CI/CD variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**.
To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier.
@@ -546,9 +546,9 @@ services:
If you wish to test your app with different PHP versions and [database management systems](../../services/index.md), you can define different `image` and `services` keywords for each test job.
-#### Variables
+#### CI/CD variables
-GitLab CI/CD allows us to use [environment variables](../../yaml/README.md#variables) in our jobs.
+GitLab CI/CD allows us to use [CI/CD variables](../../yaml/README.md#variables) in our jobs.
We defined MySQL as our database management system, which comes with a superuser root created by default.
So we should adjust the configuration of MySQL instance by defining `MYSQL_DATABASE` variable as our database name and `MYSQL_ROOT_PASSWORD` variable as the password of `root`.
@@ -567,7 +567,7 @@ variables:
#### Unit Test as the first job
-We defined the required shell scripts as an array of the [script](../../yaml/README.md#script) variable to be executed when running `unit_test` job.
+We defined the required shell scripts as an array of the [script](../../yaml/README.md#script) keyword to be executed when running `unit_test` job.
These scripts are some Artisan commands to prepare the Laravel, and, at the end of the script, we'll run the tests by `PHPUnit`.
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 87291f4e8b8..28d00362309 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -59,7 +59,7 @@ This project has three jobs:
## Store API keys
-You'll need to create two variables in **Settings > CI/CD > Environment variables** in your GitLab project:
+You'll need to create two variables in **Settings > CI/CD > Variables** in your GitLab project:
- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 1204a1ae837..5bf0b3d01be 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -50,7 +50,7 @@ This project has three jobs:
## Store API keys
-You'll need to create two variables in your project's **Settings > CI/CD > Environment variables** and do not check **Protect variable** and **Mask variable**:
+You'll need to create two CI/CD variables in your project's **Settings > CI/CD > Variables** and do not check **Protect variable** or **Mask variable**:
- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md
index f8fcb8a0437..d1fe6db3ee4 100644
--- a/doc/ci/jobs/index.md
+++ b/doc/ci/jobs/index.md
@@ -139,6 +139,23 @@ usually want the first number to be the index and the second number to be the to
[This regular expression](https://gitlab.com/gitlab-org/gitlab/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99)
evaluates the job names: `\d+[\s:\/\\]+\d+\s*`.
+### Improved job grouping
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52644) in GitLab 13.9.
+> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
+> - It's enabled on GitLab.com.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](../../administration/feature_flags.md). **(FREE SELF)**
+
+Job grouping is evaluated with an improved regular expression to group jobs by name:
+
+- `([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z`.
+
+The new implementation removes one or more `: [...]`, `X Y`, `X/Y`, or `X\Y` sequences
+from the **end** of job names only.
+
+Matching substrings occuring at the beginning or in the middle of build names are
+no longer removed.
+
## Specifying variables when running manual jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30485) in GitLab 12.2.
@@ -150,7 +167,7 @@ additional variables. To access this page, click on the **name** of the manual j
the pipeline view, *not* the play (**{play}**) button.
This is useful when you want to alter the execution of a job that uses
-[custom environment variables](../variables/README.md#custom-cicd-variables).
+[custom CI/CD variables](../variables/README.md#custom-cicd-variables).
Add a variable name (key) and value here to override the value defined in
[the UI or `.gitlab-ci.yml`](../variables/README.md#custom-cicd-variables),
for a single run of the manual job.
diff --git a/doc/ci/migration/circleci.md b/doc/ci/migration/circleci.md
index f010773d799..0a44232efd3 100644
--- a/doc/ci/migration/circleci.md
+++ b/doc/ci/migration/circleci.md
@@ -265,7 +265,7 @@ test_async:
## Contexts and variables
-CircleCI provides [Contexts](https://circleci.com/docs/2.0/contexts/) to securely pass environment variables across project pipelines. In GitLab, a [Group](../../user/group/index.md) can be created to assemble related projects together. At the group level, [variables](../variables/README.md#group-level-cicd-variables) can be stored outside the individual projects, and securely passed into pipelines across multiple projects.
+CircleCI provides [Contexts](https://circleci.com/docs/2.0/contexts/) to securely pass environment variables across project pipelines. In GitLab, a [Group](../../user/group/index.md) can be created to assemble related projects together. At the group level, [CI/CD variables](../variables/README.md#group-level-cicd-variables) can be stored outside the individual projects, and securely passed into pipelines across multiple projects.
## Orbs
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index b4bf0537a40..4c186b8a64e 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -68,7 +68,7 @@ outbound connections for upstream and downstream pipeline dependencies.
When using:
-- Variables or [`rules`](yaml/README.md#rulesif) to control job behavior, the value of
+- CI/CD Variables or [`rules`](yaml/README.md#rulesif) to control job behavior, the value of
the [`$CI_PIPELINE_SOURCE` predefined variable](variables/predefined_variables.md) is
`pipeline` for multi-project pipeline triggered through the API with `CI_JOB_TOKEN`.
- [`only/except`](yaml/README.md#onlyexcept-basic) to control job behavior, use the
@@ -114,7 +114,7 @@ the `staging` job is marked as _failed_.
When using:
-- Variables or [`rules`](yaml/README.md#rulesif) to control job behavior, the value of
+- CI/CD variables or [`rules`](yaml/README.md#rulesif) to control job behavior, the value of
the [`$CI_PIPELINE_SOURCE` predefined variable](variables/predefined_variables.md) is
`pipeline` for multi-project pipelines triggered with a bridge job (using the
[`trigger:`](yaml/README.md#trigger) keyword).
@@ -162,11 +162,11 @@ of the user that ran the trigger job in the upstream project. If the user does n
have permission to run CI/CD pipelines against the protected branch, the pipeline fails. See
[pipeline security for protected branches](pipelines/index.md#pipeline-security-on-protected-branches).
-### Passing variables to a downstream pipeline
+### Passing CI/CD variables to a downstream pipeline
#### With the `variables` keyword
-Sometimes you might want to pass variables to a downstream pipeline.
+Sometimes you might want to pass CI/CD variables to a downstream pipeline.
You can do that using the `variables` keyword, just like you would when
defining a regular job.
@@ -183,7 +183,7 @@ staging:
```
The `ENVIRONMENT` variable is passed to every job defined in a downstream
-pipeline. It is available as an environment variable when GitLab Runner picks a job.
+pipeline. It is available as a variable when GitLab Runner picks a job.
In the following configuration, the `MY_VARIABLE` variable is passed to the downstream pipeline
that is created when the `trigger-downstream` job is queued. This is because `trigger-downstream`
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index e8de038ec6b..bc1c02bc48c 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -183,7 +183,7 @@ possible to trigger another level of child pipelines.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M).
-## Pass variables to a child pipeline
+## Pass CI/CD variables to a child pipeline
-You can [pass variables to a downstream pipeline](multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline)
+You can [pass CI/CD variables to a downstream pipeline](multi_project_pipelines.md#passing-cicd-variables-to-a-downstream-pipeline)
the same way as for multi-project pipelines.
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 758f882a8be..cf8e4e71534 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -498,7 +498,7 @@ This is often preceded by other errors or warnings that specify the filename and
generated in the first place. Please check the entire job log for such messages.
If you find no helpful messages, please retry the failed job after activating
-[CI debug logging](../variables/README.md#debug-logging).
+[CI/CD debug logging](../variables/README.md#debug-logging).
This provides useful information to investigate further.
<!-- ## Troubleshooting
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 063e05fddab..9de6a1162bd 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -58,7 +58,7 @@ The process of configuring Review Apps is as follows:
1. Set up the infrastructure to host and deploy the Review Apps (check the [examples](#review-apps-examples) below).
1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a runner to do deployment.
-1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}`
+1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI/CD variable](../variables/README.md) `${CI_COMMIT_REF_NAME}`
to create dynamic environments and restrict it to run only on branches.
Alternatively, you can get a YML template for this job by [enabling review apps](#enable-review-apps-button) for your project.
1. Optionally, set a job that [manually stops](../environments/index.md#stopping-an-environment) the Review Apps.
@@ -243,7 +243,7 @@ looks for a project with code hosted in a project on GitLab.com:
</script>
```
-Ideally, you should use [environment variables](../variables/predefined_variables.md)
+Ideally, you should use [CI/CD variables](../variables/predefined_variables.md)
to replace those values at runtime when each review app is created:
- `data-project-id` is the project ID, which can be found by the `CI_PROJECT_ID`
diff --git a/doc/ci/secrets/index.md b/doc/ci/secrets/index.md
index 93275e11288..eac72bc7351 100644
--- a/doc/ci/secrets/index.md
+++ b/doc/ci/secrets/index.md
@@ -13,7 +13,7 @@ Secrets represent sensitive information your CI job needs to complete work. This
sensitive information can be items like API tokens, database credentials, or private keys.
Secrets are sourced from your secrets provider.
-Unlike CI variables, which are always presented to a job, secrets must be explicitly
+Unlike CI/CD variables, which are always presented to a job, secrets must be explicitly
required by a job. Read [GitLab CI/CD pipeline configuration reference](../yaml/README.md#secrets)
for more information about the syntax.
@@ -80,7 +80,7 @@ To configure your Vault server:
1. Configure roles on your Vault server, restricting roles to a project or namespace,
as described in [Configure Vault server roles](#configure-vault-server-roles) on this page.
-1. [Create the following CI variables](../variables/README.md#custom-cicd-variables)
+1. [Create the following CI/CD variables](../variables/README.md#custom-cicd-variables)
to provide details about your Vault server:
- `VAULT_SERVER_URL` - The URL of your Vault server, such as `https://vault.example.com:8200`.
Required.
@@ -113,8 +113,8 @@ In this example:
- `ops` - The path where the secrets engine is mounted.
After GitLab fetches the secret from Vault, the value is saved in a temporary file.
-The path to this file is stored in environment variable named `DATABASE_PASSWORD`,
-similar to [CI variables of type `file`](../variables/README.md#custom-cicd-variables-of-type-file).
+The path to this file is stored in a CI/CD variable named `DATABASE_PASSWORD`,
+similar to [variables of type `file`](../variables/README.md#custom-cicd-variables-of-type-file).
For more information about the supported syntax, read the
[`.gitlab-ci.yml` reference](../yaml/README.md#secretsvault).
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 1595907184e..5bd034cbf97 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -14,7 +14,7 @@ need it for your tests to run.
If you want to use a MySQL container, you can use [GitLab Runner](../runners/README.md) with the Docker executor.
-1. [Create variables](../variables/README.md#create-a-custom-variable-in-the-ui) for your
+1. [Create CI/CD variables](../variables/README.md#create-a-custom-variable-in-the-ui) for your
MySQL database and password by going to **Settings > CI/CD**, expanding **Variables**,
and clicking **Add Variable**.
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index d37875e1e05..16576069583 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -31,7 +31,7 @@ variables:
To set values for the `POSTGRES_DB`, `POSTGRES_USER`,
`POSTGRES_PASSWORD` and `POSTGRES_HOST_AUTH_METHOD`,
-[assign them to a variable in the user interface](../variables/README.md#create-a-custom-variable-in-the-ui),
+[assign them to a CI/CD variable in the user interface](../variables/README.md#create-a-custom-variable-in-the-ui),
then assign that variable to the corresponding variable in your
`.gitlab-ci.yml` file.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index c41a4c97d22..b6228e26175 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -604,7 +604,7 @@ You can override the value of a variable when:
1. Manually playing a job via the UI.
1. Using [push options](../../user/project/push_options.md#push-options-for-gitlab-cicd).
1. Manually triggering pipelines with [the API](../triggers/README.md#making-use-of-trigger-variables).
-1. Passing variables to a [downstream pipeline](../multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline).
+1. Passing variables to a [downstream pipeline](../multi_project_pipelines.md#passing-cicd-variables-to-a-downstream-pipeline).
These pipeline variables declared in these events take [priority over other variables](#priority-of-cicd-variables).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 00f2847a815..2abfbd3a5b4 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -396,7 +396,7 @@ include:
```
For an example of how you can include these predefined variables, and their impact on CI jobs,
-see the following [CI variable demo](https://youtu.be/4XR8gw3Pkos).
+see the following [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
#### `include:local`
@@ -1217,8 +1217,8 @@ by using `&&` or `||`, and the [variable matching operators (`==`, `!=`, `=~` an
Unlike variables in [`script`](../variables/README.md#syntax-of-cicd-variables-in-job-scripts)
sections, variables in rules expressions are always formatted as `$VARIABLE`.
-`if:` clauses are evaluated based on the values of [predefined environment variables](../variables/predefined_variables.md)
-or [custom environment variables](../variables/README.md#custom-cicd-variables).
+`if:` clauses are evaluated based on the values of [predefined CI/CD variables](../variables/predefined_variables.md)
+or [custom CI/CD variables](../variables/README.md#custom-cicd-variables).
For example:
@@ -1359,7 +1359,7 @@ if there is no `if:` statement that limits the job to branch or merge request pi
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34272) in GitLab 13.6.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/267192) in GitLab 13.7.
-Environment variables can be used in `rules:changes` expressions to determine when
+CI/CD variables can be used in `rules:changes` expressions to determine when
to add jobs to a pipeline:
```yaml
@@ -2143,7 +2143,7 @@ build_job:
artifacts: true
```
-Environment variables support for `project:`, `job:`, and `ref` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202093)
+CI/CD variable support for `project:`, `job:`, and `ref` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202093)
in GitLab 13.3. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235761) in GitLab 13.4.
For example:
@@ -2711,7 +2711,7 @@ deploy as review app:
The `deploy as review app` job is marked as a deployment to dynamically
create the `review/$CI_COMMIT_REF_NAME` environment. `$CI_COMMIT_REF_NAME`
-is an [environment variable](../variables/README.md) set by the runner. The
+is a [CI/CD variable](../variables/README.md) set by the runner. The
`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
for inclusion in URLs. If the `deploy as review app` job runs in a branch named
`pow`, this environment would be accessible with a URL like `https://review-pow.example.com/`.
@@ -2812,7 +2812,7 @@ URI-encoded `%2F`. A value made only of dots (`.`, `%2E`) is also forbidden.
You can specify a [fallback cache key](#fallback-cache-key) to use if the specified `cache:key` is not found.
-#### Fallback cache key
+##### Fallback cache key
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/1534) in GitLab Runner 13.4.
@@ -3577,7 +3577,7 @@ test:
```
Every parallel job has a `CI_NODE_INDEX` and `CI_NODE_TOTAL`
-[environment variable](../variables/README.md#predefined-cicd-variables) set.
+[predefined CI/CD variable](../variables/README.md#predefined-cicd-variables) set.
Different languages and test suites have different methods to enable parallelization.
For example, use [Semaphore Test Boosters](https://github.com/renderedtext/test-boosters)
@@ -4207,10 +4207,10 @@ release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33014) in GitLab 13.4.
-`secrets` indicates the [CI Secrets](../secrets/index.md) this job needs. It should be a hash,
-and the keys should be the names of the environment variables that are made available to the job.
+`secrets` indicates the [CI/CD Secrets](../secrets/index.md) this job needs. It should be a hash,
+and the keys should be the names of the variables that are made available to the job.
The value of each secret is saved in a temporary file. This file's path is stored in these
-environment variables.
+variables.
#### `secrets:vault` **(PREMIUM)**
diff --git a/doc/development/documentation/feature_flags.md b/doc/development/documentation/feature_flags.md
index 6aa1aed0236..c9c291abd2c 100644
--- a/doc/development/documentation/feature_flags.md
+++ b/doc/development/documentation/feature_flags.md
@@ -265,7 +265,7 @@ It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](<replace with path to>/administration/feature_flags.md)
can opt to disable it.
-To enabled it:
+To enable it:
```ruby
# For the instance
diff --git a/doc/development/documentation/styleguide/img/tier_badge.png b/doc/development/documentation/styleguide/img/tier_badge.png
index 674d869da9b..5fc38e08172 100644
--- a/doc/development/documentation/styleguide/img/tier_badge.png
+++ b/doc/development/documentation/styleguide/img/tier_badge.png
Binary files differ
diff --git a/doc/user/analytics/ci_cd_analytics.md b/doc/user/analytics/ci_cd_analytics.md
index 3b357ffd642..0f19998749d 100644
--- a/doc/user/analytics/ci_cd_analytics.md
+++ b/doc/user/analytics/ci_cd_analytics.md
@@ -42,8 +42,8 @@ performance indicators for software development teams:
production.
GitLab plans to add support for all the DORA4 metrics at the project and group levels. GitLab added
-the first metric, deployment frequency, at the project level for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts)
-and the [API]( ../../api/project_analytics.md).
+the first metric, deployment frequency, at the project and group scopes for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts),
+the [Project API]( ../../api/dora4_project_analytics.md), and the [Group API]( ../../api/dora4_group_analytics.md).
## Deployment frequency charts **(ULTIMATE)**
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 1fda26caa62..d2418c3d27f 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -466,7 +466,7 @@ Group wiki repositories can be moved through the [Group repository storage moves
There are a few limitations compared to project wikis:
- Git LFS is not supported.
-- Group wikis are not included in global search, group exports, and Geo replication.
+- Group wikis are not included in global search and Geo replication.
- Changes to group wikis don't show up in the group's activity feed.
For updates, you can follow:
@@ -870,3 +870,13 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
+
+## DORA4 analytics overview **(ULTIMATE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291747) in GitLab [Ultimate](https://about.gitlab.com/pricing/) 13.9 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
+
+Group details include the following analytics:
+
+- Deployment Frequency
+
+For more information, see [DORA4 Project Analytics API](../../api/dora4_group_analytics.md).
diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md
index 6ad550d77c3..bb7c1e26544 100644
--- a/doc/user/group/settings/import_export.md
+++ b/doc/user/group/settings/import_export.md
@@ -48,6 +48,7 @@ The following items are exported:
- Subgroups (including all the aforementioned data)
- Epics
- Events
+- Wikis **(PREMIUM SELF)** (Introduced in [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53247))
The following items are **not** exported:
diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md
index 734dee3b4c5..f935fa87d68 100644
--- a/doc/user/packages/composer_repository/index.md
+++ b/doc/user/packages/composer_repository/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Composer packages in the Package Registry
+# Composer packages in the Package Registry **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15886) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15886) in GitLab Premium 13.2.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish [Composer](https://getcomposer.org/) packages in your project's Package Registry.
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index 0f4ed62d265..c115f94b964 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Conan packages in the Package Registry
+# Conan packages in the Package Registry **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8248) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.6.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8248) in GitLab Premium 12.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish Conan packages in your project's Package Registry. Then install the
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index 627ca5c0c88..fdf0caba090 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -35,7 +35,7 @@ The following images and packages are supported.
| Docker | 11.11+ |
For a list of planned additions, view the
-[direction page](https://about.gitlab.com/direction/package/dependency_proxy/#top-vision-items).
+[direction page](https://about.gitlab.com/direction/package/#dependency-proxy).
## Enable the Dependency Proxy
diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md
index d84e629b60c..c9397f8d9f6 100644
--- a/doc/user/packages/go_proxy/index.md
+++ b/doc/user/packages/go_proxy/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Go proxy for GitLab
+# Go proxy for GitLab **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27376) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.1.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27376) in GitLab Premium 13.1.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled for GitLab.com.
> - It's not recommended for production use.
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index c43fc2664d3..828eec812fa 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Maven packages in the Package Repository
+# Maven packages in the Package Repository **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5811) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.3.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5811) in GitLab Premium 11.3.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish [Maven](https://maven.apache.org) artifacts in your project’s Package Registry.
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 93c480593ef..b1075e19b7b 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# npm packages in the Package Registry
+# npm packages in the Package Registry **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5934) in GitLab Premium 11.7.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish npm packages in your project's Package Registry. Then install the
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index 5805fb29931..5c655ac6b14 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# NuGet packages in the Package Registry
+# NuGet packages in the Package Registry **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in GitLab Premium 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish NuGet packages in your project’s Package Registry. Then, install the
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 3357d776f27..763dbee3a82 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -4,9 +4,9 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# PyPI packages in the Package Registry
+# PyPI packages in the Package Registry **(FREE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in GitLab Premium 12.10.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
Publish PyPI packages in your project’s Package Registry. Then install the
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 90875f064aa..a8ab1cbbb63 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -152,9 +152,9 @@ There are numerous [APIs](../../api/README.md) to use with your projects:
- [Traffic](../../api/project_statistics.md)
- [Variables](../../api/project_level_variables.md)
- [Aliases](../../api/project_aliases.md)
-- [Analytics](../../api/project_analytics.md)
+- [DORA4 Analytics](../../api/dora4_project_analytics.md)
-## Project activity analytics overview **(ULTIMATE SELF)**
+## DORA4 analytics overview **(ULTIMATE ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/279039) in GitLab [Ultimate](https://about.gitlab.com/pricing/) 13.7 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
@@ -162,4 +162,4 @@ Project details include the following analytics:
- Deployment Frequency
-For more information, see [Project Analytics API](../../api/project_analytics.md).
+For more information, see [DORA4 Project Analytics API](../../api/dora4_project_analytics.md).
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 75934a1a814..5a915ebef89 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -242,13 +242,32 @@ Learn how to [clone a repository through the command line](../../../gitlab-basic
Alternatively, clone directly into a code editor as documented below.
-### Clone to Apple Xcode
+### Clone and open in Apple Xcode
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/45820) in GitLab 11.0.
Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be cloned
-into Xcode using the new **Open in Xcode** button, located next to the Git URL
-used for cloning your project. The button is only shown on macOS.
+into Xcode on macOS. To do that:
+
+1. From the GitLab UI, go to the project's overview page.
+1. Click **Clone**.
+1. Select **Xcode**.
+
+The project will be cloned onto your computer in a folder of your choice and you'll
+be prompted to open in XCode.
+
+### Clone and open in Visual Studio Code
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220957) in GitLab 13.8.
+
+All projects can be cloned into Visual Studio Code. To do that:
+
+1. From the GitLab UI, go to the project's overview page.
+1. Click **Clone**.
+1. Select **VS Code**
+
+You'll be prompted to select a folder to clone the project into. When VS Code has
+successfully cloned your project, it will open the folder.
## Download Source Code
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 1b36e27f6c9..fa75d012613 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -121,6 +121,7 @@ module API
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ optional :include_retried, type: Boolean, default: false, desc: 'Includes retried jobs'
use :optional_scope
use :pagination
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index cb37841dd80..eaedd53aedb 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -227,7 +227,7 @@ module API
service.execute
status(200)
rescue Gitlab::Changelog::Error => ex
- render_api_error!("Failed to generate the changelog: #{ex.message}", 500)
+ render_api_error!("Failed to generate the changelog: #{ex.message}", 422)
end
end
end
diff --git a/lib/api/support/git_access_actor.rb b/lib/api/support/git_access_actor.rb
index 39730da1251..71395086ac2 100644
--- a/lib/api/support/git_access_actor.rb
+++ b/lib/api/support/git_access_actor.rb
@@ -37,6 +37,8 @@ module API
end
def update_last_used_at!
+ return if Feature.enabled?(:disable_ssh_key_used_tracking)
+
key&.update_last_used_at
end
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 0222ca021b7..3258d965c93 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -182,7 +182,7 @@ module Gitlab
if job.trace_chunks.any?
Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
archive_stream!(stream)
- stream.destroy!
+ destroy_stream(job) { stream.destroy! }
end
elsif current_path
File.open(current_path) do |stream|
@@ -268,7 +268,21 @@ module Gitlab
end
def trace_artifact
- job.job_artifacts_trace
+ read_trace_artifact(job) { job.job_artifacts_trace }
+ end
+
+ ##
+ # Overridden in EE
+ #
+ def destroy_stream(job)
+ yield
+ end
+
+ ##
+ # Overriden in EE
+ #
+ def read_trace_artifact(job)
+ yield
end
def being_watched_cache_key
@@ -277,3 +291,5 @@ module Gitlab
end
end
end
+
+::Gitlab::Ci::Trace.prepend_if_ee('EE::Gitlab::Ci::Trace')
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
index 8ae0ec5a78a..c1ccfae3e1f 100644
--- a/lib/gitlab/pages_transfer.rb
+++ b/lib/gitlab/pages_transfer.rb
@@ -7,16 +7,26 @@
#
module Gitlab
class PagesTransfer < ProjectTransfer
- class Async
- METHODS = %w[move_namespace move_project rename_project rename_namespace].freeze
+ METHODS = %w[move_namespace move_project rename_project rename_namespace].freeze
+ class Async
METHODS.each do |meth|
define_method meth do |*args|
+ next unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
+
PagesTransferWorker.perform_async(meth, args)
end
end
end
+ METHODS.each do |meth|
+ define_method meth do |*args|
+ next unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
+
+ super(*args)
+ end
+ end
+
def async
@async ||= Async.new
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3847742c1b5..05398f64588 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9444,6 +9444,9 @@ msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
+msgid "Date range is greater than %{quarter_days} days"
+msgstr ""
+
msgid "Day of month"
msgstr ""
@@ -20912,10 +20915,10 @@ msgstr ""
msgid "Open errors"
msgstr ""
-msgid "Open in Xcode"
+msgid "Open in file view"
msgstr ""
-msgid "Open in file view"
+msgid "Open in your IDE"
msgstr ""
msgid "Open issues"
@@ -21428,6 +21431,15 @@ msgstr ""
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr ""
+msgid "Parameter `from` must be specified"
+msgstr ""
+
+msgid "Parameter `interval` must be one of (\"%{valid_intervals}\")"
+msgstr ""
+
+msgid "Parameter `to` is before the `from` date"
+msgstr ""
+
msgid "Parent"
msgstr ""
@@ -28433,67 +28445,67 @@ msgstr ""
msgid "Suggested solutions help link"
msgstr ""
-msgid "SuggestedColors|Bright green"
+msgid "SuggestedColors|Aztec Gold"
msgstr ""
-msgid "SuggestedColors|Dark grayish cyan"
+msgid "SuggestedColors|Blue"
msgstr ""
-msgid "SuggestedColors|Dark moderate blue"
+msgid "SuggestedColors|Blue-gray"
msgstr ""
-msgid "SuggestedColors|Dark moderate orange"
+msgid "SuggestedColors|Carrot orange"
msgstr ""
-msgid "SuggestedColors|Dark moderate pink"
+msgid "SuggestedColors|Champagne"
msgstr ""
-msgid "SuggestedColors|Dark moderate violet"
+msgid "SuggestedColors|Charcoal grey"
msgstr ""
-msgid "SuggestedColors|Feijoa"
+msgid "SuggestedColors|Crimson"
msgstr ""
-msgid "SuggestedColors|Lime green"
+msgid "SuggestedColors|Dark coral"
msgstr ""
-msgid "SuggestedColors|Moderate blue"
+msgid "SuggestedColors|Dark green"
msgstr ""
-msgid "SuggestedColors|Pure red"
+msgid "SuggestedColors|Dark sea green"
msgstr ""
-msgid "SuggestedColors|Slightly desaturated blue"
+msgid "SuggestedColors|Dark violet"
msgstr ""
-msgid "SuggestedColors|Slightly desaturated green"
+msgid "SuggestedColors|Deep violet"
msgstr ""
-msgid "SuggestedColors|Soft orange"
+msgid "SuggestedColors|Gray"
msgstr ""
-msgid "SuggestedColors|Soft red"
+msgid "SuggestedColors|Green screen"
msgstr ""
-msgid "SuggestedColors|Strong pink"
+msgid "SuggestedColors|Green-cyan"
msgstr ""
-msgid "SuggestedColors|Strong red"
+msgid "SuggestedColors|Lavendar"
msgstr ""
-msgid "SuggestedColors|Strong yellow"
+msgid "SuggestedColors|Magenta-pink"
msgstr ""
-msgid "SuggestedColors|UA blue"
+msgid "SuggestedColors|Medium sea green"
msgstr ""
-msgid "SuggestedColors|Very dark desaturated blue"
+msgid "SuggestedColors|Red"
msgstr ""
-msgid "SuggestedColors|Very dark lime green"
+msgid "SuggestedColors|Rose red"
msgstr ""
-msgid "SuggestedColors|Very pale orange"
+msgid "SuggestedColors|Titanium yellow"
msgstr ""
msgid "Suggestion is not applicable as the suggestion was not found."
@@ -32536,6 +32548,9 @@ msgstr ""
msgid "Visit settings page"
msgstr ""
+msgid "Visual Studio Code"
+msgstr ""
+
msgid "VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:"
msgstr ""
@@ -33369,6 +33384,9 @@ msgstr ""
msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
msgstr ""
+msgid "Xcode"
+msgstr ""
+
msgid "YYYY-MM-DD"
msgstr ""
@@ -33648,6 +33666,9 @@ msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
+msgid "You do not have permission to access deployment frequencies"
+msgstr ""
+
msgid "You do not have permission to leave this %{namespaceType}."
msgstr ""
diff --git a/package.json b/package.json
index 85339e7ce73..eb39884a1d9 100644
--- a/package.json
+++ b/package.json
@@ -124,7 +124,6 @@
"smooshpack": "^0.0.62",
"sortablejs": "^1.10.2",
"sql.js": "^0.4.0",
- "stickyfilljs": "^2.1.0",
"string-hash": "1.1.3",
"style-loader": "^1.3.0",
"swagger-ui-dist": "^3.32.4",
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index 9927ef0903a..08a54f112bb 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -185,6 +185,7 @@ RSpec.describe Import::BulkImportsController do
describe 'POST create' do
let(:instance_url) { "http://fake-intance" }
+ let(:bulk_import) { create(:bulk_import) }
let(:pat) { "fake-pat" }
before do
@@ -201,12 +202,13 @@ RSpec.describe Import::BulkImportsController do
expect_next_instance_of(
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
- expect(service).to receive(:execute)
+ allow(service).to receive(:execute).and_return(bulk_import)
end
post :create, params: { bulk_import: bulk_import_params }
expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq({ id: bulk_import.id }.to_json)
end
end
end
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index bd5d07fda71..d21f602f90c 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Repositories::GitHttpController do
context 'when project_statistics_sync feature flag is disabled' do
before do
- stub_feature_flags(project_statistics_sync: false)
+ stub_feature_flags(project_statistics_sync: false, disable_git_http_fetch_writes: false)
end
it 'updates project statistics async for projects' do
@@ -47,6 +47,8 @@ RSpec.describe Repositories::GitHttpController do
end
it 'updates project statistics sync for projects' do
+ stub_feature_flags(disable_git_http_fetch_writes: false)
+
expect { send_request }.to change {
Projects::DailyStatisticsFinder.new(container).total_fetch_count
}.from(0).to(1)
@@ -56,6 +58,29 @@ RSpec.describe Repositories::GitHttpController do
let(:namespace) { project.namespace }
subject { send_request }
+
+ before do
+ stub_feature_flags(disable_git_http_fetch_writes: false)
+ end
+ end
+
+ context 'when disable_git_http_fetch_writes is enabled' do
+ before do
+ stub_feature_flags(disable_git_http_fetch_writes: true)
+ end
+
+ it 'does not increment statistics' do
+ expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+ expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
+
+ send_request
+ end
+
+ it 'does not record onboarding progress' do
+ expect(OnboardingProgressService).not_to receive(:new)
+
+ send_request
+ end
end
end
end
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index 4a6585e3f2b..ab056dd26e8 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Ci::JobsFinder, '#execute' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:job_1) { create(:ci_build) }
let_it_be(:job_2) { create(:ci_build, :running) }
- let_it_be(:job_3) { create(:ci_build, :success, pipeline: pipeline) }
+ let_it_be(:job_3) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
let(:params) { {} }
@@ -95,4 +95,35 @@ RSpec.describe Ci::JobsFinder, '#execute' do
end
end
end
+
+ context 'when pipeline is present' do
+ before_all do
+ project.add_maintainer(user)
+ job_3.update!(retried: true)
+ end
+
+ let_it_be(:job_4) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
+
+ subject { described_class.new(current_user: user, pipeline: pipeline, params: params).execute }
+
+ it 'does not return retried jobs by default' do
+ expect(subject).to match_array([job_4])
+ end
+
+ context 'when include_retried is false' do
+ let(:params) { { include_retried: false } }
+
+ it 'does not return retried jobs' do
+ expect(subject).to match_array([job_4])
+ end
+ end
+
+ context 'when include_retried is true' do
+ let(:params) { { include_retried: true } }
+
+ it 'returns retried jobs' do
+ expect(subject).to match_array([job_3, job_4])
+ end
+ end
+ end
end
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 82868daec98..0f659fa1dab 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -5,16 +5,8 @@ require 'spec_helper'
RSpec.describe DeploymentsFinder do
subject { described_class.new(params).execute }
- let_it_be(:project) { create(:project, :public, :test_repo) }
- let(:params) { { project: project } }
-
describe "#execute" do
- it 'returns all deployments by default' do
- deployments = create_list(:deployment, 2, :success, project: project)
- is_expected.to match_array(deployments)
- end
-
- context 'when project is missing' do
+ context 'when project or group is missing' do
let(:params) { {} }
it 'returns nothing' do
@@ -22,147 +14,150 @@ RSpec.describe DeploymentsFinder do
end
end
- describe 'filtering' do
- context 'when updated_at filters are specified' do
- let(:params) { { project: project, updated_before: 1.day.ago, updated_after: 3.days.ago } }
- let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
- let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
+ context 'at project scope' do
+ let_it_be(:project) { create(:project, :public, :test_repo) }
+ let(:base_params) { { project: project } }
- it 'returns deployments with matched updated_at' do
- is_expected.to match_array([deployment_1])
- end
- end
+ describe 'filtering' do
+ context 'when updated_at filters are specified' do
+ let(:params) { { **base_params, updated_before: 1.day.ago, updated_after: 3.days.ago } }
+ let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
- context 'when the environment name is specified' do
- let!(:environment1) { create(:environment, project: project) }
- let!(:environment2) { create(:environment, project: project) }
- let!(:deployment1) do
- create(:deployment, project: project, environment: environment1)
+ it 'returns deployments with matched updated_at' do
+ is_expected.to match_array([deployment_1])
+ end
end
- let!(:deployment2) do
- create(:deployment, project: project, environment: environment2)
+ context 'when the environment name is specified' do
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:deployment1) do
+ create(:deployment, project: project, environment: environment1)
+ end
+
+ let!(:deployment2) do
+ create(:deployment, project: project, environment: environment2)
+ end
+
+ let(:params) { { **base_params, environment: environment1.name } }
+
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
end
- let(:params) { { project: project, environment: environment1.name } }
+ context 'when the deployment status is specified' do
+ let!(:deployment1) { create(:deployment, :success, project: project) }
+ let!(:deployment2) { create(:deployment, :failed, project: project) }
+ let(:params) { { **base_params, status: 'success' } }
- it 'returns deployments for the given environment' do
- is_expected.to match_array([deployment1])
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
end
- end
- context 'when the deployment status is specified' do
- let!(:deployment1) { create(:deployment, :success, project: project) }
- let!(:deployment2) { create(:deployment, :failed, project: project) }
- let(:params) { { project: project, status: 'success' } }
+ context 'when using an invalid deployment status' do
+ let(:params) { { **base_params, status: 'kittens' } }
- it 'returns deployments for the given environment' do
- is_expected.to match_array([deployment1])
+ it 'raises ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
end
end
- context 'when using an invalid deployment status' do
- let(:params) { { project: project, status: 'kittens' } }
+ describe 'ordering' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:params) { { **base_params, order_by: order_by, sort: sort } }
+
+ let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: Time.now) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 2.hours.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 1.hour.ago) }
+
+ where(:order_by, :sort, :ordered_deployments) do
+ 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'finished_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'finished_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
+ end
- it 'raises ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ with_them do
+ it 'returns the deployments ordered' do
+ expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ end
end
end
- context 'when filtering by finished time' do
- let!(:deployment_1) { create(:deployment, :success, project: project, finished_at: 2.days.ago) }
- let!(:deployment_2) { create(:deployment, :success, project: project, finished_at: 4.days.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, finished_at: 5.hours.ago) }
-
- context 'when filtering by finished_after and finished_before' do
- let(:params) { { project: project, finished_after: 3.days.ago, finished_before: 1.day.ago } }
+ describe 'transform `created_at` sorting to `id` sorting' do
+ let(:params) { { **base_params, order_by: 'created_at', sort: 'asc' } }
- it { is_expected.to match_array([deployment_1]) }
+ it 'sorts by only one column' do
+ expect(subject.order_values.size).to eq(1)
end
- context 'when the finished_before parameter is missing' do
- let(:params) { { project: project, finished_after: 3.days.ago } }
-
- it { is_expected.to match_array([deployment_1, deployment_3]) }
+ it 'sorts by `id`' do
+ expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
end
+ end
- context 'when finished_after is missing' do
- let(:params) { { project: project, finished_before: 1.day.ago } }
+ describe 'tie-breaker for `finished_at` sorting' do
+ let(:params) { { **base_params, order_by: 'updated_at', sort: 'asc' } }
- it 'does not apply any filters on finished time' do
- is_expected.to match_array([deployment_1, deployment_2, deployment_3])
- end
+ it 'sorts by two columns' do
+ expect(subject.order_values.size).to eq(2)
end
- end
- end
- describe 'ordering' do
- using RSpec::Parameterized::TableSyntax
-
- let(:params) { { project: project, order_by: order_by, sort: sort } }
-
- let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: 3.hours.ago) }
- let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 1.hour.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 2.hours.ago) }
-
- where(:order_by, :sort, :ordered_deployments) do
- 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
- 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
- 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
- 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
- 'finished_at' | 'asc' | [:deployment_1, :deployment_3, :deployment_2]
- 'finished_at' | 'desc' | [:deployment_2, :deployment_3, :deployment_1]
- 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
- end
+ it 'adds `id` sorting as the second order column' do
+ order_value = subject.order_values[1]
- with_them do
- it 'returns the deployments ordered' do
- expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ expect(order_value.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql)
end
- end
- end
- describe 'transform `created_at` sorting to `id` sorting' do
- let(:params) { { project: project, order_by: 'created_at', sort: 'asc' } }
+ it 'uses the `id DESC` as tie-breaker when ordering' do
+ updated_at = Time.now
- it 'sorts by only one column' do
- expect(subject.order_values.size).to eq(1)
- end
+ deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at)
+ deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at)
+ deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at)
- it 'sorts by `id`' do
- expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
+ expect(subject).to eq([deployment_3, deployment_2, deployment_1])
+ end
end
- end
- describe 'tie-breaker for `updated_at` sorting' do
- let(:params) { { project: project, order_by: 'updated_at', sort: 'asc' } }
+ context 'when filtering by finished time' do
+ let!(:deployment_1) { create(:deployment, :success, project: project, finished_at: 2.days.ago) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, finished_at: 4.days.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, finished_at: 5.hours.ago) }
- it 'sorts by two columns' do
- expect(subject.order_values.size).to eq(2)
- end
+ context 'when filtering by finished_after and finished_before' do
+ let(:params) { { **base_params, finished_after: 3.days.ago, finished_before: 1.day.ago } }
- it 'adds `id` sorting as the second order column' do
- order_value = subject.order_values[1]
+ it { is_expected.to match_array([deployment_1]) }
+ end
- expect(order_value.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql)
- end
+ context 'when the finished_before parameter is missing' do
+ let(:params) { { **base_params, finished_after: 3.days.ago } }
- it 'uses the `id DESC` as tie-breaker when ordering' do
- updated_at = Time.now
+ it { is_expected.to match_array([deployment_1, deployment_3]) }
+ end
- deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at)
- deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at)
- deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at)
+ context 'when finished_after is missing' do
+ let(:params) { { **base_params, finished_before: 3.days.ago } }
- expect(subject).to eq([deployment_3, deployment_2, deployment_1])
+ it { is_expected.to match_array([deployment_2]) }
+ end
end
end
end
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 2a2d51cf6b4..a56f761269a 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -141,8 +141,8 @@ describe('Commit pipeline status component', () => {
expect(findLink().attributes('href')).toEqual(mockCiStatus.details_path);
});
- it('renders CI icon', () => {
- expect(findCiIcon().attributes('title')).toEqual('Pipeline: pending');
+ it('renders CI icon with the correct title and status', () => {
+ expect(findCiIcon().attributes('title')).toEqual('Pipeline: passed');
expect(findCiIcon().props('status')).toEqual(mockCiStatus);
});
});
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index f3280e216ff..7117c9a1c7a 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -6,9 +6,10 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin, name: 'root') }
- let(:namespace) { create(:namespace, name: 'gitlab-test' )}
- let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:admin) { create(:admin, name: 'root') }
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:mr) { create(:merge_request, source_project: project) }
before(:all) do
clean_frontend_fixtures('api/merge_requests')
@@ -21,4 +22,16 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
+
+ it 'api/merge_requests/versions.json' do
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", admin)
+
+ expect(response).to be_successful
+ end
+
+ it 'api/merge_requests/changes.json' do
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", admin)
+
+ expect(response).to be_successful
+ end
end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 4270e38afcb..b4b7f0e332f 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -12,7 +12,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:user) { create(:user, developer_projects: [project], email: commit.author_email) }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) }
let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) }
- let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') }
+ let!(:pipeline_without_commit) { create(:ci_pipeline, status: :success, project: project, sha: '0000') }
render_views
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index 0436dc8a04f..600bd5fe9e1 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -1,19 +1,33 @@
import MockAdapter from 'axios-mock-adapter';
+import { range } from 'lodash';
+import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';
+import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
getMergeRequestData,
getMergeRequestChanges,
getMergeRequestVersions,
+ openMergeRequestChanges,
openMergeRequest,
} from '~/ide/stores/actions/merge_request';
+import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;
+const createMergeRequestChange = (path) => ({
+ new_path: path,
+ path,
+});
+const createMergeRequestChangesCount = (n) =>
+ range(n).map((i) => createMergeRequestChange(`loremispum_${i}.md`));
+
+const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`;
+
jest.mock('~/flash');
describe('IDE store merge request actions', () => {
@@ -353,6 +367,72 @@ describe('IDE store merge request actions', () => {
});
});
+ describe('openMergeRequestChanges', () => {
+ it.each`
+ desc | changes | entries
+ ${'with empty changes'} | ${[]} | ${{}}
+ ${'with changes not matching entries'} | ${[{ new_path: '123.md' }]} | ${{ '456.md': {} }}
+ `('$desc, does nothing', ({ changes, entries }) => {
+ const state = { entries };
+
+ return testAction({
+ action: openMergeRequestChanges,
+ state,
+ payload: changes,
+ expectedActions: [],
+ expectedMutations: [],
+ });
+ });
+
+ it('updates views and opens mr changes', () => {
+ // This is the payload sent to the action
+ const changesPayload = createMergeRequestChangesCount(15);
+
+ // Remove some items from the payload to use for entries
+ const changes = changesPayload.slice(1, 14);
+
+ const entries = changes.reduce(
+ (acc, { path }) => Object.assign(acc, { [path]: path, type: 'blob' }),
+ {},
+ );
+ const pathsToOpen = changes.slice(0, MAX_MR_FILES_AUTO_OPEN).map((x) => x.new_path);
+
+ return testAction({
+ action: openMergeRequestChanges,
+ state: { entries, getUrlForPath: testGetUrlForPath },
+ payload: changesPayload,
+ expectedActions: [
+ { type: 'updateActivityBarView', payload: leftSidebarViews.review.name },
+ // Only activates first file
+ { type: 'router/push', payload: testGetUrlForPath(pathsToOpen[0]) },
+ { type: 'setFileActive', payload: pathsToOpen[0] },
+ // Fetches data for other files
+ ...pathsToOpen.slice(1).map((path) => ({
+ type: 'getFileData',
+ payload: { path, makeFileActive: false },
+ })),
+ ...pathsToOpen.slice(1).map((path) => ({
+ type: 'getRawFileData',
+ payload: { path },
+ })),
+ ],
+ expectedMutations: [
+ ...changes.map((change) => ({
+ type: types.SET_FILE_MERGE_REQUEST_CHANGE,
+ payload: {
+ file: entries[change.new_path],
+ mrChange: change,
+ },
+ })),
+ ...pathsToOpen.map((path) => ({
+ type: types.TOGGLE_FILE_OPEN,
+ payload: path,
+ })),
+ ],
+ });
+ });
+ });
+
describe('openMergeRequest', () => {
const mr = {
projectId: TEST_PROJECT,
@@ -409,7 +489,6 @@ describe('IDE store merge request actions', () => {
case 'getFiles':
case 'getMergeRequestVersions':
case 'getBranchData':
- case 'setFileMrChange':
return Promise.resolve();
default:
return originalDispatch(type, payload);
@@ -445,6 +524,7 @@ describe('IDE store merge request actions', () => {
],
['getMergeRequestVersions', mr],
['getMergeRequestChanges', mr],
+ ['openMergeRequestChanges', testMergeRequestChanges.changes],
]);
})
.then(done)
@@ -454,9 +534,11 @@ describe('IDE store merge request actions', () => {
it('updates activity bar view and gets file data, if changes are found', (done) => {
store.state.entries.foo = {
type: 'blob',
+ path: 'foo',
};
store.state.entries.bar = {
type: 'blob',
+ path: 'bar',
};
testMergeRequestChanges.changes = [
@@ -467,24 +549,9 @@ describe('IDE store merge request actions', () => {
openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
- 'updateActivityBarView',
- leftSidebarViews.review.name,
+ 'openMergeRequestChanges',
+ testMergeRequestChanges.changes,
);
-
- testMergeRequestChanges.changes.forEach((change, i) => {
- expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', {
- file: store.state.entries[change.new_path],
- mrChange: change,
- });
-
- expect(store.dispatch).toHaveBeenCalledWith('getFileData', {
- path: change.new_path,
- makeFileActive: i === 0,
- openFile: true,
- });
- });
-
- expect(store.state.openFiles.length).toBe(testMergeRequestChanges.changes.length);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index b94e2c2bd74..4d3d2c41bbe 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -28,6 +28,7 @@ const FAKE_ENDPOINTS = {
status: '/fake_status_url',
availableNamespaces: '/fake_available_namespaces',
createBulkImport: '/fake_create_bulk_import',
+ jobs: '/fake_jobs',
};
describe('Bulk import resolvers', () => {
@@ -109,6 +110,11 @@ describe('Bulk import resolvers', () => {
),
).toBe(true);
});
+
+ it('starts polling when request completes', async () => {
+ const [statusPoller] = StatusPoller.mock.instances;
+ expect(statusPoller.startPolling).toHaveBeenCalled();
+ });
});
it.each`
@@ -215,7 +221,7 @@ describe('Bulk import resolvers', () => {
});
it('sets group status to STARTED when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
@@ -224,16 +230,6 @@ describe('Bulk import resolvers', () => {
expect(results[0].status).toBe(STATUSES.STARTED);
});
- it('starts polling when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
- await client.mutate({
- mutation: importGroupMutation,
- variables: { sourceGroupId: GROUP_ID },
- });
- const [statusPoller] = StatusPoller.mock.instances;
- expect(statusPoller.startPolling).toHaveBeenCalled();
- });
-
it('resets status to NONE if request fails', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
index 3ae6fd43513..a5fc4e18a02 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
@@ -1,191 +1,113 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import { createMockClient } from 'mock-apollo-client';
-import waitForPromises from 'helpers/wait_for_promises';
-
+import MockAdapter from 'axios-mock-adapter';
+import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
-import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
-import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
-import { generateFakeEntry } from '../fixtures';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+jest.mock('visibilityjs');
jest.mock('~/flash');
+jest.mock('~/lib/utils/poll');
jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({
SourceGroupsManager: jest.fn().mockImplementation(function mock() {
this.setImportStatus = jest.fn();
+ this.findByImportId = jest.fn();
}),
}));
-const TEST_POLL_INTERVAL = 1000;
-const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
+const FAKE_POLL_PATH = '/fake/poll/path';
+const CLIENT_MOCK = {};
describe('Bulk import status poller', () => {
let poller;
- let clientMock;
-
- const listQueryCacheCalls = () =>
- clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery);
-
- const generateFakeGroups = (statuses) =>
- statuses.map((status, idx) => generateFakeEntry({ status, id: idx }));
-
- const writeFakeGroupsQuery = (nodes) => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: {
- __typename: clientTypenames.BulkImportSourceGroupConnection,
- nodes,
- pageInfo: {
- __typename: clientTypenames.BulkImportPageInfo,
- ...FAKE_PAGE_INFO,
- },
- },
- },
- });
- };
+ let mockAdapter;
+
+ const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH);
beforeEach(() => {
- clientMock = createMockClient({
- cache: new InMemoryCache({
- fragmentMatcher: { match: () => true },
- }),
- });
-
- jest.spyOn(clientMock, 'readQuery');
-
- poller = new StatusPoller({
- client: clientMock,
- interval: TEST_POLL_INTERVAL,
- });
+ mockAdapter = new MockAdapter(axios);
+ mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
+ poller = new StatusPoller({ client: CLIENT_MOCK, pollPath: FAKE_POLL_PATH });
+ });
+
+ it('creates source group manager with proper client', () => {
+ expect(SourceGroupsManager.mock.calls).toHaveLength(1);
+ const [[{ client }]] = SourceGroupsManager.mock.calls;
+ expect(client).toBe(CLIENT_MOCK);
+ });
+
+ it('creates poller with proper config', () => {
+ expect(Poll.mock.calls).toHaveLength(1);
+ const [[pollConfig]] = Poll.mock.calls;
+ expect(typeof pollConfig.method).toBe('string');
+
+ const pollOperation = pollConfig.resource[pollConfig.method];
+ expect(typeof pollOperation).toBe('function');
+ });
+
+ it('invokes axios when polling is performed', async () => {
+ const [[pollConfig]] = Poll.mock.calls;
+ const pollOperation = pollConfig.resource[pollConfig.method];
+ expect(getPollHistory()).toHaveLength(0);
+
+ pollOperation();
+ await axios.waitForAll();
+
+ expect(getPollHistory()).toHaveLength(1);
});
- describe('general behavior', () => {
- beforeEach(() => {
- writeFakeGroupsQuery([]);
- });
-
- it('does not perform polling when constructed', () => {
- jest.runOnlyPendingTimers();
- expect(listQueryCacheCalls()).toHaveLength(0);
- });
-
- it('immediately start polling when requested', async () => {
- await poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('constantly polls when started', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(2);
-
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(3);
- });
-
- it('does not start polling when requested multiple times', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('stops polling when requested', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- poller.stopPolling();
- jest.runOnlyPendingTimers();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('does not query server when list is empty', async () => {
- jest.spyOn(clientMock, 'query');
- poller.startPolling();
- expect(clientMock.query).not.toHaveBeenCalled();
- });
+ it('subscribes to visibility changes', () => {
+ expect(Visibility.change).toHaveBeenCalled();
});
- it('does not query server when no groups have STARTED status', async () => {
- writeFakeGroupsQuery(generateFakeGroups([STATUSES.NONE, STATUSES.FINISHED]));
+ it.each`
+ isHidden | action
+ ${true} | ${'stop'}
+ ${false} | ${'restart'}
+ `('$action polling when hidden is $isHidden', ({ action, isHidden }) => {
+ const [pollInstance] = Poll.mock.instances;
+ const [[changeHandler]] = Visibility.change.mock.calls;
+ Visibility.hidden.mockReturnValue(isHidden);
+ expect(pollInstance[action]).not.toHaveBeenCalled();
+
+ changeHandler();
+
+ expect(pollInstance[action]).toHaveBeenCalled();
+ });
+
+ it('does not perform polling when constructed', async () => {
+ await axios.waitForAll();
+
+ expect(getPollHistory()).toHaveLength(0);
+ });
+
+ it('immediately start polling when requested', async () => {
+ const [pollInstance] = Poll.mock.instances;
- jest.spyOn(clientMock, 'query');
poller.startPolling();
- expect(clientMock.query).not.toHaveBeenCalled();
+
+ expect(pollInstance.makeRequest).toHaveBeenCalled();
+ });
+
+ it('when error occurs shows flash with error', () => {
+ const [[pollConfig]] = Poll.mock.calls;
+ pollConfig.errorCallback();
+ expect(createFlash).toHaveBeenCalled();
});
- describe('when there are groups which have STARTED status', () => {
- const TARGET_NAMESPACE = 'root';
-
- const STARTED_GROUP_1 = generateFakeEntry({
- status: STATUSES.STARTED,
- id: 'started1',
- });
-
- const STARTED_GROUP_2 = generateFakeEntry({
- status: STATUSES.STARTED,
- id: 'started2',
- });
-
- const NOT_STARTED_GROUP = generateFakeEntry({
- status: STATUSES.NONE,
- id: 'not_started',
- });
-
- it('query server only for groups with STATUSES.STARTED', async () => {
- writeFakeGroupsQuery([STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2]);
-
- clientMock.query = jest.fn().mockResolvedValue({ data: {} });
- poller.startPolling();
-
- expect(clientMock.query).toHaveBeenCalledTimes(1);
- await waitForPromises();
- const [[doc]] = clientMock.query.mock.calls;
- const { selections } = doc.query.definitions[0].selectionSet;
- expect(selections.every((field) => field.name.value === 'group')).toBeTruthy();
- expect(selections).toHaveLength(2);
- expect(selections.map((sel) => sel.arguments[0].value.value)).toStrictEqual([
- `${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`,
- `${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`,
- ]);
- });
-
- it('updates statuses only for groups in response', async () => {
- writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]);
-
- clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } });
- poller.startPolling();
- await waitForPromises();
- const [managerInstance] = SourceGroupsManager.mock.instances;
- expect(managerInstance.setImportStatus).toHaveBeenCalledTimes(1);
- expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
- expect.objectContaining({ id: STARTED_GROUP_1.id }),
- STATUSES.FINISHED,
- );
- });
-
- describe('when error occurs', () => {
- beforeEach(() => {
- writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]);
-
- clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error'));
- poller.startPolling();
- return waitForPromises();
- });
-
- it('reports an error', () => {
- expect(createFlash).toHaveBeenCalled();
- });
-
- it('continues polling', async () => {
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(2);
- });
- });
+ it('when success response arrives updates relevant group status', () => {
+ const FAKE_ID = 5;
+ const [[pollConfig]] = Poll.mock.calls;
+ const [managerInstance] = SourceGroupsManager.mock.instances;
+ managerInstance.findByImportId.mockReturnValue({ id: FAKE_ID });
+
+ pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] });
+
+ expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
+ expect.objectContaining({ id: FAKE_ID }),
+ STATUSES.FINISHED,
+ );
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 77faceaaec7..811303a5624 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,7 +1,9 @@
-import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { chunk } from 'lodash';
import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
@@ -20,29 +22,29 @@ import { pipelineWithStages, stageReply, users, mockSearch, branches } from './m
jest.mock('~/flash');
-describe('Pipelines', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
-
- preloadFixtures(jsonFixtureName);
+const mockProjectPath = 'twitter/flight';
+const mockProjectId = '21';
+const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
+const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json');
+const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
- let pipelines;
+describe('Pipelines', () => {
let wrapper;
let mock;
+ let origWindowLocation;
const paths = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
- resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
- newPipelinePath: '/twitter/flight/pipelines/new',
+ resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
+ newPipelinePath: `${mockProjectPath}/pipelines/new`,
};
const noPermissions = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
@@ -56,101 +58,140 @@ describe('Pipelines', () => {
...paths,
};
- const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
- const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
- const findNavigationTabs = () => wrapper.find(NavigationTabs);
- const findNavigationControls = () => wrapper.find(NavigationControls);
- const findTab = (tab) => findByTestId(`pipelines-tab-${tab}`);
-
- const findRunPipelineButton = () => findByTestId('run-pipeline-button');
- const findCiLintButton = () => findByTestId('ci-lint-button');
- const findCleanCacheButton = () => findByTestId('clear-cache-button');
- const findStagesDropdown = () => findByTestId('mini-pipeline-graph-dropdown-toggle');
-
- const findEmptyState = () => wrapper.find(EmptyState);
- const findBlankState = () => wrapper.find(BlankState);
-
- const findTablePagination = () => wrapper.find(TablePagination);
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
+ const findNavigationControls = () => wrapper.findComponent(NavigationControls);
+ const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+ const findBlankState = () => wrapper.findComponent(BlankState);
+ const findTablePagination = () => wrapper.findComponent(TablePagination);
+
+ const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
+ const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
+ const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
+ const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
+ const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const createComponent = (props = defaultProps) => {
- wrapper = mount(PipelinesComponent, {
- propsData: {
- store: new Store(),
- projectId: '21',
- params: {},
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mount(PipelinesComponent, {
+ propsData: {
+ store: new Store(),
+ projectId: mockProjectId,
+ endpoint: mockPipelinesEndpoint,
+ params: {},
+ ...props,
+ },
+ }),
+ );
};
- beforeEach(() => {
+ beforeAll(() => {
+ origWindowLocation = window.location;
delete window.location;
+ window.location = { search: '' };
+ });
+
+ afterAll(() => {
+ window.location = origWindowLocation;
});
beforeEach(() => {
- window.location = { search: '' };
mock = new MockAdapter(axios);
- pipelines = getJSONFixture(jsonFixtureName);
+ jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
});
afterEach(() => {
wrapper.destroy();
- mock.restore();
+ mock.reset();
+ window.history.pushState.mockReset();
});
- describe('With permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
- createComponent();
- return waitForPromises();
- });
+ describe('when pipelines are not yet loaded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ it('shows loading state when the app is loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
- it('renders Run Pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('does not display tabs when the first request has not yet been made', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ });
+
+ it('does not display buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are pipelines in the project', () => {
+ beforeEach(() => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse);
+ });
+
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('renders CI Lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
});
- it('renders Clear Runner Cache button', () => {
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ it('does not render buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
+
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
createComponent();
+ await waitForPromises();
+ });
- return waitForPromises();
+ it('should set up navigation tabs', () => {
+ expect(findNavigationTabs().props('tabs')).toEqual([
+ { name: 'All', scope: 'all', count: '3', isActive: true },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
+ });
+
+ it('should render other navigation tabs', () => {
+ expect(findTab('finished').text()).toBe('Finished');
+ expect(findTab('branches').text()).toBe('Branches');
+ expect(findTab('tags').text()).toBe('Tags');
+ });
+
+ it('shows navigation controls', () => {
+ expect(findNavigationControls().exists()).toBe(true);
});
it('renders Run Pipeline link', () => {
@@ -165,549 +206,513 @@ describe('Pipelines', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
});
- it('renders tab empty state', () => {
- expect(findBlankState().text()).toBe('There are currently no pipelines.');
- });
-
- it('renders tab empty state finished scope', () => {
- wrapper.vm.scope = 'finished';
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- return nextTick().then(() => {
- expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ describe('when user goes to a tab', () => {
+ const goToTab = (tab) => {
+ findNavigationTabs().vm.$emit('onChangeTab', tab);
+ };
- return waitForPromises();
- });
+ describe('when the scope in the tab has pipelines', () => {
+ const mockFinishedPipeline = mockPipelinesResponse.pipelines[0];
- it('renders empty state', () => {
- expect(findEmptyState().find('h4').text()).toBe('Build with confidence');
- expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
- });
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
+ .reply(200, {
+ pipelines: [mockFinishedPipeline],
+ count: mockPipelinesResponse.count,
+ });
- it('does not render tabs nor buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
- });
+ goToTab('finished');
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
+ });
- return waitForPromises();
- });
+ it('should filter pipelines', async () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`);
+ });
- it('renders buttons', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=finished&page=1`,
+ );
+ });
+ });
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
- });
+ describe('when the scope in the tab is empty', () => {
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
+ .reply(200, {
+ pipelines: [],
+ count: mockPipelinesResponse.count,
+ });
- it('renders error state', () => {
- expect(findBlankState().text()).toContain('There was an error fetching the pipelines.');
- });
- });
- });
+ goToTab('branches');
- describe('Without permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ await waitForPromises();
+ });
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('should filter pipelines', async () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=branches&page=1`,
+ );
+ });
+ });
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ describe('when user triggers a filtered search', () => {
+ const mockFilteredPipeline = mockPipelinesResponse.pipelines[1];
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ let expectedParams;
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
- });
- });
+ beforeEach(async () => {
+ expectedParams = {
+ page: '1',
+ scope: 'all',
+ username: 'root',
+ ref: 'master',
+ status: 'pending',
+ };
+
+ mock
+ .onGet(mockPipelinesEndpoint, {
+ params: expectedParams,
+ })
+ .replyOnce(200, {
+ pipelines: [mockFilteredPipeline],
+ count: mockPipelinesResponse.count,
+ });
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ await waitForPromises();
});
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual(expectedParams);
+ });
- return waitForPromises();
- });
+ it('renders filtered pipelines', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`,
+ );
+ });
});
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ describe('when user triggers a filtered search with raw text', () => {
+ beforeEach(async () => {
+ findFilteredSearch().vm.$emit('submit', ['rawText']);
- it('renders tab empty state', () => {
- expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.');
- });
- });
+ await waitForPromises();
+ });
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
});
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('displays a warning message if raw text search is used', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all`,
+ );
+ });
});
+ });
+ });
- it('renders empty state without button to set CI', () => {
- expect(findEmptyState().text()).toBe(
- 'This project is not currently set up to run pipelines.',
- );
+ describe('when there are multiple pages of pipelines', () => {
+ const mockPageSize = 2;
+ const mockPageHeaders = ({ page = 1 } = {}) => {
+ return {
+ 'X-PER-PAGE': `${mockPageSize}`,
+ 'X-PREV-PAGE': `${page - 1}`,
+ 'X-PAGE': `${page}`,
+ 'X-NEXT-PAGE': `${page + 1}`,
+ };
+ };
+ const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize);
+
+ const goToPage = (page) => {
+ findTablePagination().find(GlPagination).vm.$emit('input', page);
+ };
+
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(
+ 200,
+ {
+ pipelines: firstPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 1 }),
+ );
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply(
+ 200,
+ {
+ pipelines: secondPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 2 }),
+ );
- expect(findEmptyState().find(GlButton).exists()).toBeFalsy();
- });
+ createComponent();
- it('does not render tabs or buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ await waitForPromises();
});
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
-
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
+ it('shows the first page of pipelines', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(firstPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`);
+ });
- return waitForPromises();
- });
+ it('should not update browser bar', () => {
+ expect(window.history.pushState).not.toHaveBeenCalled();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ describe('when user goes to next page', () => {
+ beforeEach(async () => {
+ goToPage(2);
+ await waitForPromises();
});
- it('does not renders buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(secondPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`);
});
- it('renders error state', () => {
- expect(wrapper.find('.empty-state').text()).toContain(
- 'There was an error fetching the pipelines.',
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=2&scope=all`,
);
});
});
});
- describe('successful request', () => {
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ describe('when pipelines can be polled', () => {
+ beforeEach(() => {
+ const emptyResponse = {
+ pipelines: [],
+ count: { all: '0' },
+ };
- createComponent();
- return waitForPromises();
- });
+ // Mock no pipelines in the first attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .replyOnce(200, emptyResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ // Mock pipelines in the next attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ });
- it('should render table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ describe('data is loaded for the first time', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
});
- it('should set up navigation tabs', () => {
- expect(findNavigationTabs().props('tabs')).toEqual([
- { name: 'All', scope: 'all', count: '3', isActive: true },
- { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
- { name: 'Branches', scope: 'branches', isActive: false },
- { name: 'Tags', scope: 'tags', isActive: false },
- ]);
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
});
- it('should render navigation tabs', () => {
- expect(findTab('all').html()).toContain('All');
- expect(findTab('finished').text()).toContain('Finished');
- expect(findTab('branches').text()).toContain('Branches');
- expect(findTab('tags').text()).toContain('Tags');
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(0);
});
- it('should make an API request when using tabs', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
-
- return waitForPromises().then(() => {
- findTab('finished').trigger('click');
-
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'finished',
- page: '1',
- });
+ describe('data is loaded for a second time', () => {
+ beforeEach(async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
});
- });
- describe('with pagination', () => {
- it('should make an API request when using pagination', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
-
- return waitForPromises()
- .then(() => {
- // Mock pagination
- wrapper.vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ });
- return nextTick();
- })
- .then(() => {
- wrapper.find('.next-page-item').trigger('click');
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'all',
- page: '2',
- });
- });
+ it('is loading after a time', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
});
});
- describe('User Interaction', () => {
- let updateContentMock;
-
- beforeEach(() => {
- jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
- });
-
+ describe('when no pipelines exist', () => {
beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
-
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
-
- return waitForPromises();
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
+ });
});
- describe('when user changes tabs', () => {
- it('should set page to 1', () => {
- findNavigationTabs().vm.$emit('onChangeTab', 'running');
+ describe('when CI is enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ it('renders tab with count of "0"', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- });
- describe('when user changes page', () => {
- it('should update page and keep scope', () => {
- findTablePagination().vm.change(4);
+ it('renders Run Pipeline link', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
+ it('renders CI Lint link', () => {
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
});
- });
- describe('updates results when a staged is clicked', () => {
- beforeEach(() => {
- const copyPipeline = { ...pipelineWithStages };
- copyPipeline.id += 1;
- mock
- .onGet('twitter/flight/pipelines.json')
- .reply(
- 200,
- {
- pipelines: [pipelineWithStages],
- count: {
- all: 1,
- finished: 1,
- pending: 0,
- running: 0,
- },
- },
- {
- 'POLL-INTERVAL': 100,
- },
- )
- .onGet(pipelineWithStages.details.stages[0].dropdown_path)
- .reply(200, stageReply);
+ it('renders Clear Runner Cache button', () => {
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- createComponent();
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
- describe('when a request is being made', () => {
- it('stops polling, cancels the request, & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- return waitForPromises()
- .then(() => {
- wrapper.vm.isMakingRequest = true;
- findStagesDropdown().trigger('click');
- })
- .then(() => {
- expect(cancelMock).toHaveBeenCalled();
- expect(stopMock).toHaveBeenCalled();
- expect(restartMock).toHaveBeenCalled();
- });
+ it('renders tab empty state finished scope', async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
});
- });
- describe('when no request is being made', () => {
- it('stops polling & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ findNavigationTabs().vm.$emit('onChangeTab', 'finished');
- return waitForPromises()
- .then(() => {
- findStagesDropdown().trigger('click');
- expect(stopMock).toHaveBeenCalled();
- })
- .then(() => {
- expect(restartMock).toHaveBeenCalled();
- });
- });
- });
- });
- });
+ await waitForPromises();
- describe('Rendered content', () => {
- beforeEach(() => {
- createComponent();
+ expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
+ });
});
- describe('displays different content', () => {
- it('shows loading state when the app is loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ describe('when CI is not enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
});
- it('shows error state when app has error', () => {
- wrapper.vm.hasError = true;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().props('message')).toBe(
- 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
- );
- });
+ it('renders empty state', () => {
+ expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe(
+ 'Build with confidence',
+ );
+ expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain(
+ 'GitLab CI/CD can automatically build, test, and deploy your code.',
+ );
+ expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
+ expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
});
- it('shows table list when app has pipelines', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
-
- return nextTick().then(() => {
- expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true);
- });
+ it('does not render tabs nor buttons', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
+ });
- it('shows empty tab when app does not have pipelines but project has pipelines', () => {
- wrapper.vm.state.count.all = 10;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ describe('when CI is not enabled and user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('shows empty tab when project has CI', () => {
- wrapper.vm.isLoading = false;
+ it('renders empty state without button to set CI', () => {
+ expect(findEmptyState().text()).toBe(
+ 'This project is not currently set up to run pipelines.',
+ );
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ expect(findEmptyState().find(GlButton).exists()).toBe(false);
});
- it('shows empty state when project does not have pipelines nor CI', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- });
+ it('does not render tabs or buttons', () => {
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
});
- describe('displays tabs', () => {
- it('returns true when state is loading & has already made the first request', () => {
- wrapper.vm.isLoading = true;
- wrapper.vm.hasMadeRequest = true;
+ describe('when CI is enabled and user has no permissions', () => {
+ beforeEach(() => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ return waitForPromises();
});
- it('returns true when state is tableList & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
- wrapper.vm.hasMadeRequest = true;
-
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders tab with count of "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- it('returns true when state is error & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = true;
- wrapper.vm.hasMadeRequest = true;
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
+ });
+ });
- it('returns true when state is empty tab & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.count.all = 10;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a pipeline with stages exists', () => {
+ describe('updates results when a staged is clicked', () => {
+ let stopMock;
+ let restartMock;
+ let cancelMock;
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
- });
+ beforeEach(() => {
+ mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply(
+ 200,
+ {
+ pipelines: [pipelineWithStages],
+ count: { all: '1' },
+ },
+ {
+ 'POLL-INTERVAL': 100,
+ },
+ );
+ mock.onGet(pipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply);
- it('returns false when has not made first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ createComponent();
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
- });
+ stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
});
- it('returns false when state is empty state', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a request is being made', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
+ await waitForPromises();
});
- });
- });
- describe('displays buttons', () => {
- it('returns true when it has paths & has made the first request', () => {
- wrapper.vm.hasMadeRequest = true;
+ it('stops polling, cancels the request, & restarts polling', async () => {
+ // Mock init a polling cycle
+ wrapper.vm.poll.options.notificationCallback(true);
+
+ findStagesDropdown().trigger('click');
+
+ await waitForPromises();
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(true);
+ expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
- });
- it('returns false when it has not made the first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ it('stops polling & restarts polling', async () => {
+ findStagesDropdown().trigger('click');
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(false);
+ expect(cancelMock).not.toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
});
});
});
- describe('Pipeline filters', () => {
- let updateContentMock;
-
- beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
+ describe('when pipelines cannot be loaded', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(500, {});
+ });
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
- return waitForPromises();
- });
+ await waitForPromises();
+ });
- it('updates request data and query params on filter submit', async () => {
- const expectedQueryParams = {
- page: '1',
- scope: 'all',
- username: 'root',
- ref: 'master',
- status: 'pending',
- };
+ it('renders tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toBe('All');
+ });
- findFilteredSearch().vm.$emit('submit', mockSearch);
- await nextTick();
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
- it('does not add query params if raw text search is used', async () => {
- const expectedQueryParams = { page: '1', scope: 'all' };
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
- findFilteredSearch().vm.$emit('submit', ['rawText']);
- await nextTick();
+ await waitForPromises();
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
- });
+ it('renders tabs', () => {
+ expect(findTab('all').text()).toBe('All');
+ });
+
+ it('renders buttons', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
- it('displays a warning message if raw text search is used', () => {
- findFilteredSearch().vm.$emit('submit', ['rawText']);
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index 4b4d265800b..322e632da02 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -79,7 +79,7 @@ describe('DropdownCreateLabelComponent', () => {
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
- expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
+ expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 153, 102);');
});
it('renders color input element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 648ba84fe8f..73716d4edf3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -16,27 +16,27 @@ export const mockLabels = [
];
export const mockSuggestedColors = {
- '#0033CC': 'UA blue',
- '#428BCA': 'Moderate blue',
- '#44AD8E': 'Lime green',
- '#A8D695': 'Feijoa',
- '#5CB85C': 'Slightly desaturated green',
- '#69D100': 'Bright green',
- '#004E00': 'Very dark lime green',
- '#34495E': 'Very dark desaturated blue',
- '#7F8C8D': 'Dark grayish cyan',
- '#A295D6': 'Slightly desaturated blue',
- '#5843AD': 'Dark moderate blue',
- '#8E44AD': 'Dark moderate violet',
- '#FFECDB': 'Very pale orange',
- '#AD4363': 'Dark moderate pink',
- '#D10069': 'Strong pink',
- '#CC0033': 'Strong red',
- '#FF0000': 'Pure red',
- '#D9534F': 'Soft red',
- '#D1D100': 'Strong yellow',
- '#F0AD4E': 'Soft orange',
- '#AD8D43': 'Dark moderate orange',
+ '#009966': 'Green-cyan',
+ '#8fbc8f': 'Dark sea green',
+ '#3cb371': 'Medium sea green',
+ '#00b140': 'Green screen',
+ '#013220': 'Dark green',
+ '#6699cc': 'Blue-gray',
+ '#0000ff': 'Blue',
+ '#e6e6fa': 'Lavendar',
+ '#9400d3': 'Dark violet',
+ '#330066': 'Deep violet',
+ '#808080': 'Gray',
+ '#36454f': 'Charcoal grey',
+ '#f7e7ce': 'Champagne',
+ '#c21e56': 'Rose red',
+ '#cc338b': 'Magenta-pink',
+ '#dc143c': 'Crimson',
+ '#ff0000': 'Red',
+ '#cd5b45': 'Dark coral',
+ '#eee600': 'Titanium yellow',
+ '#ed9121': 'Carrot orange',
+ '#c39953': 'Aztec Gold',
};
export const mockConfig = {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index 9697d6c30f2..85a14226585 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -50,25 +50,25 @@ export const mockConfig = {
};
export const mockSuggestedColors = {
- '#0033CC': 'UA blue',
- '#428BCA': 'Moderate blue',
- '#44AD8E': 'Lime green',
- '#A8D695': 'Feijoa',
- '#5CB85C': 'Slightly desaturated green',
- '#69D100': 'Bright green',
- '#004E00': 'Very dark lime green',
- '#34495E': 'Very dark desaturated blue',
- '#7F8C8D': 'Dark grayish cyan',
- '#A295D6': 'Slightly desaturated blue',
- '#5843AD': 'Dark moderate blue',
- '#8E44AD': 'Dark moderate violet',
- '#FFECDB': 'Very pale orange',
- '#AD4363': 'Dark moderate pink',
- '#D10069': 'Strong pink',
- '#CC0033': 'Strong red',
- '#FF0000': 'Pure red',
- '#D9534F': 'Soft red',
- '#D1D100': 'Strong yellow',
- '#F0AD4E': 'Soft orange',
- '#AD8D43': 'Dark moderate orange',
+ '#009966': 'Green-cyan',
+ '#8fbc8f': 'Dark sea green',
+ '#3cb371': 'Medium sea green',
+ '#00b140': 'Green screen',
+ '#013220': 'Dark green',
+ '#6699cc': 'Blue-gray',
+ '#0000ff': 'Blue',
+ '#e6e6fa': 'Lavendar',
+ '#9400d3': 'Dark violet',
+ '#330066': 'Deep violet',
+ '#808080': 'Gray',
+ '#36454f': 'Charcoal grey',
+ '#f7e7ce': 'Champagne',
+ '#c21e56': 'Rose red',
+ '#cc338b': 'Magenta-pink',
+ '#dc143c': 'Crimson',
+ '#ff0000': 'Red',
+ '#cd5b45': 'Dark coral',
+ '#eee600': 'Titanium yellow',
+ '#ed9121': 'Carrot orange',
+ '#c39953': 'Aztec Gold',
};
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 8d5d047b146..9e6bafc1297 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -69,7 +69,7 @@ const openFileRow = (row) => {
row.click();
};
-const findAndTraverseToPath = async (path, index = 0, row = null) => {
+export const findAndTraverseToPath = async (path, index = 0, row = null) => {
if (!path) {
return row;
}
@@ -110,6 +110,12 @@ const findAndClickRootAction = async (name) => {
button.click();
};
+/**
+ * Drop leading "/-/ide" and file path from the current URL
+ */
+export const getBaseRoute = (url = window.location.pathname) =>
+ url.replace(/^\/-\/ide/, '').replace(/\/-\/.*$/, '');
+
export const clickPreviewMarkdown = () => {
screen.getByText('Preview Markdown').click();
};
diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js
index 67d490b2b2e..173a9610c84 100644
--- a/spec/frontend_integration/ide/helpers/start.js
+++ b/spec/frontend_integration/ide/helpers/start.js
@@ -4,11 +4,12 @@ import Editor from '~/ide/lib/editor';
import extendStore from '~/ide/stores/extend';
import { IDE_DATASET } from './mock_data';
-export default (container, { isRepoEmpty = false, path = '' } = {}) => {
+export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => {
+ const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum';
+ const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`;
+
global.jsdom.reconfigure({
- url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum${
- isRepoEmpty ? '-empty' : ''
- }/tree/master/-/${path}`,
+ url: `${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`,
});
const el = document.createElement('div');
diff --git a/spec/frontend_integration/ide/user_opens_mr_spec.js b/spec/frontend_integration/ide/user_opens_mr_spec.js
new file mode 100644
index 00000000000..9cf0ff5da56
--- /dev/null
+++ b/spec/frontend_integration/ide/user_opens_mr_spec.js
@@ -0,0 +1,60 @@
+import { basename } from 'path';
+import { getMergeRequests, getMergeRequestWithChanges } from 'test_helpers/fixtures';
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
+import * as ideHelper from './helpers/ide_helper';
+import startWebIDE from './helpers/start';
+
+const getRelevantChanges = () =>
+ getMergeRequestWithChanges().changes.filter((x) => !x.deleted_file);
+
+describe('IDE: User opens Merge Request', () => {
+ useOverclockTimers();
+
+ let vm;
+ let container;
+ let changes;
+
+ beforeEach(async () => {
+ const [{ iid: mrId }] = getMergeRequests();
+
+ changes = getRelevantChanges();
+
+ setFixtures('<div class="webide-container"></div>');
+ container = document.querySelector('.webide-container');
+
+ vm = startWebIDE(container, { mrId });
+
+ await ideHelper.waitForTabToOpen(basename(changes[0].new_path));
+ await ideHelper.waitForMonacoEditor();
+ });
+
+ afterEach(async () => {
+ vm.$destroy();
+ vm = null;
+ });
+
+ const findAllTabs = () => Array.from(document.querySelectorAll('.multi-file-tab'));
+ const findAllTabsData = () =>
+ findAllTabs().map((el) => ({
+ title: el.getAttribute('title'),
+ text: el.textContent.trim(),
+ }));
+
+ it('shows first change as active in file tree', async () => {
+ const firstPath = changes[0].new_path;
+ const row = await ideHelper.findAndTraverseToPath(firstPath);
+
+ expect(row).toHaveClass('is-open');
+ expect(row).toHaveClass('is-active');
+ });
+
+ it('opens other changes', () => {
+ // We only show first 10 changes
+ const expectedTabs = changes.slice(0, 10).map((x) => ({
+ title: `${ideHelper.getBaseRoute()}/-/${x.new_path}/`,
+ text: basename(x.new_path),
+ }));
+
+ expect(findAllTabsData()).toEqual(expectedTabs);
+ });
+});
diff --git a/spec/frontend_integration/test_helpers/fixtures.js b/spec/frontend_integration/test_helpers/fixtures.js
index 4c0143534c5..b2768440607 100644
--- a/spec/frontend_integration/test_helpers/fixtures.js
+++ b/spec/frontend_integration/test_helpers/fixtures.js
@@ -31,6 +31,12 @@ export const getBranch = factory.json(() =>
export const getMergeRequests = factory.json(() =>
require('test_fixtures/api/merge_requests/get.json'),
);
+export const getMergeRequestWithChanges = factory.json(() =>
+ require('test_fixtures/api/merge_requests/changes.json'),
+);
+export const getMergeRequestVersions = factory.json(() =>
+ require('test_fixtures/api/merge_requests/versions.json'),
+);
export const getRepositoryFiles = factory.json(() =>
require('test_fixtures/projects_json/files.json'),
);
diff --git a/spec/frontend_integration/test_helpers/mock_server/index.js b/spec/frontend_integration/test_helpers/mock_server/index.js
index 2aebdefaafb..20cb441daa7 100644
--- a/spec/frontend_integration/test_helpers/mock_server/index.js
+++ b/spec/frontend_integration/test_helpers/mock_server/index.js
@@ -4,6 +4,8 @@ import {
getEmptyProject,
getBranch,
getMergeRequests,
+ getMergeRequestWithChanges,
+ getMergeRequestVersions,
getRepositoryFiles,
getBlobReadme,
getBlobImage,
@@ -16,6 +18,8 @@ export const createMockServerOptions = () => ({
project: Model,
branch: Model,
mergeRequest: Model,
+ mergeRequestChange: Model,
+ mergeRequestVersion: Model,
file: Model,
userPermission: Model,
},
@@ -30,6 +34,8 @@ export const createMockServerOptions = () => ({
projects: [getProject(), getEmptyProject()],
branches: [getBranch()],
mergeRequests: getMergeRequests(),
+ mergeRequestChanges: [getMergeRequestWithChanges()],
+ mergeRequestVersions: getMergeRequestVersions(),
filesRaw: [
{
raw: getBlobReadme(),
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/404.js b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
index bc8edba927e..54183f1189c 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/404.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
@@ -1,3 +1,5 @@
+import { Response } from 'miragejs';
+
export default (server) => {
['get', 'post', 'put', 'delete', 'patch'].forEach((method) => {
server[method]('*', () => {
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
index de37aa98eee..e6e09121fd4 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
@@ -20,4 +20,22 @@ export default (server) => {
return result.models;
});
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid', (schema, request) => {
+ const mr = schema.mergeRequests.findBy({ iid: request.params.mid });
+
+ return mr.attrs;
+ });
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid/versions', (schema, request) => {
+ const versions = schema.mergeRequestVersions.where({ merge_request_id: request.params.mid });
+
+ return versions.models;
+ });
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid/changes', (schema, request) => {
+ const mrWithChanges = schema.mergeRequestChanges.findBy({ iid: request.params.mid });
+
+ return mrWithChanges.attrs;
+ });
};
diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb
index 8acd2562ea8..206abaf34ce 100644
--- a/spec/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/graphql/mutations/merge_requests/update_spec.rb
@@ -12,10 +12,11 @@ RSpec.describe Mutations::MergeRequests::Update do
describe '#resolve' do
let(:attributes) { { title: 'new title', description: 'new description', target_branch: 'new-branch' } }
+ let(:arguments) { attributes }
let(:mutated_merge_request) { subject[:merge_request] }
subject do
- mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, **attributes)
+ mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, **arguments)
end
it_behaves_like 'permission level for merge request mutation is correctly verified'
@@ -61,6 +62,24 @@ RSpec.describe Mutations::MergeRequests::Update do
expect(mutated_merge_request).to have_attributes(attributes)
end
end
+
+ context 'when closing the MR' do
+ let(:arguments) { { state_event: ::Types::MergeRequestStateEventEnum.values['CLOSED'].value } }
+
+ it 'closes the MR' do
+ expect(mutated_merge_request).to be_closed
+ end
+ end
+
+ context 'when re-opening the MR' do
+ let(:arguments) { { state_event: ::Types::MergeRequestStateEventEnum.values['OPEN'].value } }
+
+ it 'closes the MR' do
+ merge_request.close!
+
+ expect(mutated_merge_request).to be_open
+ end
+ end
end
end
end
diff --git a/spec/graphql/types/merge_request_state_event_enum_spec.rb b/spec/graphql/types/merge_request_state_event_enum_spec.rb
new file mode 100644
index 00000000000..94214b29755
--- /dev/null
+++ b/spec/graphql/types/merge_request_state_event_enum_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeRequestNewState'] do
+ it 'has the appropriate values' do
+ expect(described_class.values).to contain_exactly(
+ ['OPEN', have_attributes(value: 'reopen')],
+ ['CLOSED', have_attributes(value: 'close')]
+ )
+ end
+end
diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb
index 143cc6e56ee..a09cabf4cd7 100644
--- a/spec/lib/api/support/git_access_actor_spec.rb
+++ b/spec/lib/api/support/git_access_actor_spec.rb
@@ -152,6 +152,10 @@ RSpec.describe API::Support::GitAccessActor do
end
describe '#update_last_used_at!' do
+ before do
+ stub_feature_flags(disable_ssh_key_used_tracking: false)
+ end
+
context 'when initialized with a User' do
let(:user) { build(:user) }
@@ -170,6 +174,14 @@ RSpec.describe API::Support::GitAccessActor do
subject.update_last_used_at!
end
+
+ it 'does not update `last_used_at` when the functionality is disabled' do
+ stub_feature_flags(disable_ssh_key_used_tracking: true)
+
+ expect(key).not_to receive(:update_last_used_at)
+
+ subject.update_last_used_at!
+ end
end
end
end
diff --git a/spec/lib/gitlab/pages_transfer_spec.rb b/spec/lib/gitlab/pages_transfer_spec.rb
index 4f0ee76b244..552a2e0701c 100644
--- a/spec/lib/gitlab/pages_transfer_spec.rb
+++ b/spec/lib/gitlab/pages_transfer_spec.rb
@@ -8,13 +8,24 @@ RSpec.describe Gitlab::PagesTransfer do
context 'when receiving an allowed method' do
it 'schedules a PagesTransferWorker', :aggregate_failures do
- described_class::Async::METHODS.each do |meth|
+ described_class::METHODS.each do |meth|
expect(PagesTransferWorker)
.to receive(:perform_async).with(meth, %w[foo bar])
async.public_send(meth, 'foo', 'bar')
end
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ described_class::METHODS.each do |meth|
+ expect(PagesTransferWorker)
+ .not_to receive(:perform_async)
+
+ async.public_send(meth, 'foo', 'bar')
+ end
+ end
end
context 'when receiving a private method' do
@@ -59,6 +70,15 @@ RSpec.describe Gitlab::PagesTransfer do
expect(subject.public_send(meth, *args)).to be(false)
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ subject.public_send(meth, *args)
+
+ expect(File.exist?(config_path_before)).to be(true)
+ expect(File.exist?(config_path_after)).to be(false)
+ end
end
describe '#move_namespace' do
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index 9110fdeda52..5cb84ee131a 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Clusters::AgentToken do
it { is_expected.to belong_to(:agent).class_name('Clusters::Agent') }
+ it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
describe '#token' do
it 'is generated on save' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 52f6b4f2586..01da379e001 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -510,6 +510,10 @@ RSpec.describe CommitStatus do
end
describe '#group_name' do
+ before do
+ stub_feature_flags(simplified_commit_status_group_name: false)
+ end
+
using RSpec::Parameterized::TableSyntax
let(:commit_status) do
@@ -557,6 +561,58 @@ RSpec.describe CommitStatus do
is_expected.to eq(group_name)
end
end
+
+ context 'with simplified_commit_status_group_name' do
+ before do
+ stub_feature_flags(simplified_commit_status_group_name: true)
+ end
+
+ where(:name, :group_name) do
+ 'rspec1' | 'rspec1'
+ 'rspec1 0 1' | 'rspec1'
+ 'rspec1 0/2' | 'rspec1'
+ 'rspec:windows' | 'rspec:windows'
+ 'rspec:windows 0' | 'rspec:windows 0'
+ 'rspec:windows 0 2/2' | 'rspec:windows 0'
+ 'rspec:windows 0 test' | 'rspec:windows 0 test'
+ 'rspec:windows 0 test 2/2' | 'rspec:windows 0 test'
+ 'rspec:windows 0 1 2/2' | 'rspec:windows'
+ 'rspec:windows 0 1 [aws] 2/2' | 'rspec:windows'
+ 'rspec:windows 0 1 name [aws] 2/2' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0 1 name' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0 1 name 1/2' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0/1' | 'rspec:windows'
+ 'rspec:windows 0/1 name' | 'rspec:windows 0/1 name'
+ 'rspec:windows 0/1 name 1/2' | 'rspec:windows 0/1 name'
+ 'rspec:windows 0:1' | 'rspec:windows'
+ 'rspec:windows 0:1 name' | 'rspec:windows 0:1 name'
+ 'rspec:windows 10000 20000' | 'rspec:windows'
+ 'rspec:windows 0 : / 1' | 'rspec:windows'
+ 'rspec:windows 0 : / 1 name' | 'rspec:windows 0 : / 1 name'
+ '0 1 name ruby' | '0 1 name ruby'
+ '0 :/ 1 name ruby' | '0 :/ 1 name ruby'
+ 'rspec: [aws]' | 'rspec'
+ 'rspec: [aws] 0/1' | 'rspec'
+ 'rspec: [aws, max memory]' | 'rspec'
+ 'rspec:linux: [aws, max memory, data]' | 'rspec:linux'
+ 'rspec: [inception: [something, other thing], value]' | 'rspec'
+ 'rspec:windows 0/1: [name, other]' | 'rspec:windows'
+ 'rspec:windows: [name, other] 0/1' | 'rspec:windows'
+ 'rspec:windows: [name, 0/1] 0/1' | 'rspec:windows'
+ 'rspec:windows: [0/1, name]' | 'rspec:windows'
+ 'rspec:windows: [, ]' | 'rspec:windows'
+ 'rspec:windows: [name]' | 'rspec:windows'
+ 'rspec:windows: [name,other]' | 'rspec:windows'
+ end
+
+ with_them do
+ it "#{params[:name]} puts in #{params[:group_name]}" do
+ commit_status.name = name
+
+ is_expected.to eq(group_name)
+ end
+ end
+ end
end
describe '#detailed_status' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index a2624a54668..68d12f51d4b 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -396,6 +396,26 @@ RSpec.describe Deployment do
end
end
+ describe '.finished_before' do
+ let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
+ let!(:deployment2) { create(:deployment, finished_at: Time.current) }
+
+ it 'filters deployments by finished_at' do
+ expect(described_class.finished_before(1.hour.ago))
+ .to eq([deployment1])
+ end
+ end
+
+ describe '.finished_after' do
+ let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
+ let!(:deployment2) { create(:deployment, finished_at: Time.current) }
+
+ it 'filters deployments by finished_at' do
+ expect(described_class.finished_after(1.hour.ago))
+ .to eq([deployment2])
+ end
+ end
+
describe 'with_deployable' do
subject { described_class.with_deployable }
@@ -408,22 +428,6 @@ RSpec.describe Deployment do
end
end
- describe 'finished_between' do
- subject { described_class.finished_between(start_time, end_time) }
-
- let_it_be(:start_time) { DateTime.new(2017) }
- let_it_be(:end_time) { DateTime.new(2019) }
- let_it_be(:deployment_2016) { create(:deployment, finished_at: DateTime.new(2016)) }
- let_it_be(:deployment_2017) { create(:deployment, finished_at: DateTime.new(2017)) }
- let_it_be(:deployment_2018) { create(:deployment, finished_at: DateTime.new(2018)) }
- let_it_be(:deployment_2019) { create(:deployment, finished_at: DateTime.new(2019)) }
- let_it_be(:deployment_2020) { create(:deployment, finished_at: DateTime.new(2020)) }
-
- it 'retrieves deployments that finished between the specified times' do
- is_expected.to contain_exactly(deployment_2017, deployment_2018)
- end
- end
-
describe 'visible' do
subject { described_class.visible }
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 767b5704851..a9afbd8bd72 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe API::Ci::Pipelines do
let(:query) { {} }
let(:api_user) { user }
let_it_be(:job) do
- create(:ci_build, :success, pipeline: pipeline,
+ create(:ci_build, :success, name: 'build', pipeline: pipeline,
artifacts_expire_at: 1.day.since)
end
@@ -405,6 +405,38 @@ RSpec.describe API::Ci::Pipelines do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
end.not_to exceed_all_query_limit(control_count)
end
+
+ context 'pipeline has retried jobs' do
+ before_all do
+ job.update!(retried: true)
+ end
+
+ let_it_be(:successor) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
+
+ it 'does not return retried jobs by default' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+
+ context 'when include_retried is false' do
+ let(:query) { { include_retried: false } }
+
+ it 'does not return retried jobs' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'when include_retried is true' do
+ let(:query) { { include_retried: true } }
+
+ it 'returns retried jobs' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response[0]['name']).to eq(json_response[1]['name'])
+ end
+ end
+ end
end
context 'no pipeline is found' do
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index cc1d990ad8f..ace73e49c7c 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -687,7 +687,7 @@ RSpec.describe API::Repositories do
}
)
- expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Failed to generate the changelog: oops')
end
end
diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb
index e1703b09f97..fdc2f5e1a04 100644
--- a/spec/serializers/ci/dag_pipeline_entity_spec.rb
+++ b/spec/serializers/ci/dag_pipeline_entity_spec.rb
@@ -73,11 +73,11 @@ RSpec.describe Ci::DagPipelineEntity do
end
end
- it 'performs the smallest number of queries' do
+ it 'performs the smallest number of queries', :request_store do
log = ActiveRecord::QueryRecorder.new { subject }
- # stages, project, builds, build_needs
- expect(log.count).to eq 4
+ # stages, project, builds, build_needs, feature_flag
+ expect(log.count).to eq 5
end
it 'contains all the data' do
diff --git a/spec/services/ci/daily_build_group_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb
index e878e55454d..e58a5de26a1 100644
--- a/spec/services/ci/daily_build_group_report_result_service_spec.rb
+++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb
@@ -5,9 +5,10 @@ require 'spec_helper'
RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
let_it_be(:group) { create(:group, :private) }
let_it_be(:pipeline) { create(:ci_pipeline, project: create(:project, group: group), created_at: '2020-02-06 00:01:10') }
- let_it_be(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
- let_it_be(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
+ let_it_be(:rspec_job) { create(:ci_build, pipeline: pipeline, name: 'rspec 3/3', coverage: 80) }
+ let_it_be(:karma_job) { create(:ci_build, pipeline: pipeline, name: 'karma 2/2', coverage: 90) }
let_it_be(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
+
let(:coverages) { Ci::DailyBuildGroupReportResult.all }
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
@@ -41,8 +42,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
end
context 'when there are multiple builds with the same group name that report coverage' do
- let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: '1/2 test', coverage: 70) }
- let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: '2/2 test', coverage: 80) }
+ let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: 'test 1/2', coverage: 70) }
+ let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: 'test 2/2', coverage: 80) }
it 'creates daily code coverage record with the average as the value' do
described_class.new.execute(pipeline)
@@ -70,8 +71,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
)
end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec 4/4', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma 3/3', coverage: 92) }
before do
# Create the existing daily code coverage records
@@ -110,8 +111,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
)
end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec 4/4', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma 3/3', coverage: 92) }
before do
# Create the existing daily code coverage records
diff --git a/spec/workers/pages_transfer_worker_spec.rb b/spec/workers/pages_transfer_worker_spec.rb
index 248a3713bf6..7d17461bc5a 100644
--- a/spec/workers/pages_transfer_worker_spec.rb
+++ b/spec/workers/pages_transfer_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe PagesTransferWorker do
describe '#perform' do
- Gitlab::PagesTransfer::Async::METHODS.each do |meth|
+ Gitlab::PagesTransfer::METHODS.each do |meth|
context "when method is #{meth}" do
let(:args) { [1, 2, 3] }
diff --git a/yarn.lock b/yarn.lock
index 699bf42395b..3d8f9242f54 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11089,11 +11089,6 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
-stickyfilljs@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.1.0.tgz#46dabb599d8275d185bdb97db597f86a2e3afa7b"
- integrity sha512-LkG0BXArL5HbW2O09IAXfnBQfpScgGqJuUDUrI3Ire5YKjRz/EhakIZEJogHwgXeQ4qnTicM9sK9uYfWN11qKg==
-
stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"