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>2023-11-09 00:09:31 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-09 00:09:31 +0300
commit11f9ca7e2413e4520f02c4c1b82cec0bce3789bf (patch)
treed8081c72b9a40b745b248eed09e7ed4a47069518
parent2308cd50203f5b377e4d6e03d017066507beacdf (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS115
-rw-r--r--app/assets/javascripts/admin/users/components/app.vue63
-rw-r--r--app/assets/javascripts/admin/users/constants.js4
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue3
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js2
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js45
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue (renamed from app/assets/javascripts/admin/users/components/user_avatar.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/users_table.vue (renamed from app/assets/javascripts/admin/users/components/users_table.vue)68
-rw-r--r--app/controllers/projects/jobs_controller.rb5
-rw-r--r--app/graphql/types/permission_types/ci/pipeline.rb1
-rw-r--r--app/serializers/ci/job_entity.rb2
-rw-r--r--app/services/ci/catalog/resources/release_service.rb46
-rw-r--r--app/services/ci/retry_job_service.rb4
-rw-r--r--app/services/personal_access_tokens/rotate_service.rb9
-rw-r--r--app/services/releases/create_service.rb12
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml8
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb14
-rw-r--r--config/feature_flags/development/create_deployment_only_for_processable_jobs.yml8
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/testing/browser_performance_testing.md1
-rw-r--r--doc/topics/autodevops/cicd_variables.md3
-rw-r--r--doc/topics/autodevops/customize.md5
-rw-r--r--doc/user/infrastructure/clusters/connect/new_gke_cluster.md6
-rw-r--r--lib/api/ci/pipelines.rb2
-rw-r--r--lib/api/personal_access_tokens.rb8
-rw-r--r--lib/api/resource_access_tokens.rb6
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb9
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb1
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml3
-rw-r--r--lib/tasks/gitlab/tw/codeowners.rake35
-rw-r--r--qa/qa/page/admin/overview/users/index.rb2
-rw-r--r--qa/qa/runtime/path.rb4
-rw-r--r--qa/qa/service/docker_run/base.rb8
-rw-r--r--qa/qa/service/docker_run/gitlab.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb6
-rw-r--r--spec/frontend/admin/users/components/app_spec.js85
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js141
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js21
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js66
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js2
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js24
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js90
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/users_table/mock_data.js23
-rw-r--r--spec/frontend/vue_shared/components/users_table/user_avatar_spec.js (renamed from spec/frontend/admin/users/components/user_avatar_spec.js)46
-rw-r--r--spec/frontend/vue_shared/components/users_table/users_table_spec.js95
-rw-r--r--spec/graphql/types/permission_types/ci/pipeline_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/ansi2json/state_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb28
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb372
-rw-r--r--spec/policies/ci/build_policy_spec.rb2
-rw-r--r--spec/requests/api/ci/jobs_spec.rb4
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb12
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb16
-rw-r--r--spec/serializers/ci/job_entity_spec.rb2
-rw-r--r--spec/services/ci/catalog/resources/release_service_spec.rb60
-rw-r--r--spec/services/ci/retry_job_service_spec.rb16
-rw-r--r--spec/services/releases/create_service_spec.rb9
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/workers/ci/initial_pipeline_process_worker_spec.rb26
65 files changed, 915 insertions, 851 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 5bd781f4277..a86bc9417df 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -759,89 +759,39 @@ lib/gitlab/checks/**
/doc/ci/services/ @fneill
/doc/ci/test_cases/ @msedlakjakubowski
/doc/ci/testing/code_quality.md @rdickenson
-/doc/development/activitypub/ @msedlakjakubowski
-/doc/development/advanced_search.md @ashrafkhamis
-/doc/development/ai_features/ @sselhorn
-/doc/development/application_limits.md @axil
-/doc/development/audit_event_guide/ @eread
-/doc/development/auto_devops.md @phillipwells
-/doc/development/avoiding_required_stops.md @axil
-/doc/development/backend/create_source_code_be/ @msedlakjakubowski
-/doc/development/build_test_package.md @axil
-/doc/development/bulk_import.md @eread @ashrafkhamis
-/doc/development/cached_queries.md @jglassman1
-/doc/development/cascading_settings.md @jglassman1
-/doc/development/cells/ @lciutacu
-/doc/development/chatops_on_gitlabcom.md @phillipwells
-/doc/development/cicd/ @marcel.amirault
-/doc/development/cloud_connector/ @jglassman1
-/doc/development/code_intelligence/ @aqualls
-/doc/development/code_owners/ @msedlakjakubowski
-/doc/development/code_suggestions/ @jglassman1
-/doc/development/contributing/verify/ @marcel.amirault
-/doc/development/database/ @aqualls
-/doc/development/database/filtering_by_label.md @msedlakjakubowski
-/doc/development/database/multiple_databases.md @lciutacu
-/doc/development/database_review.md @aqualls
-/doc/development/developing_with_solargraph.md @msedlakjakubowski
-/doc/development/distribution/ @axil
+/doc/development/advanced_search.md @gitlab-org/search-team/migration-maintainers
+/doc/development/application_limits.md @gitlab-org/distribution
+/doc/development/audit_event_guide/ @gitlab-org/govern/security-policies-frontend @gitlab-org/govern/threat-insights-frontend-team @gitlab-org/govern/threat-insights-backend-team
+/doc/development/avoiding_required_stops.md @gitlab-org/distribution
+/doc/development/build_test_package.md @gitlab-org/distribution
+/doc/development/cascading_settings.md @gitlab-org/govern/authentication/approvers
+/doc/development/cells/ @abdwdd @alexpooley @manojmj
+/doc/development/cicd/ @gitlab-org/maintainers/cicd-verify
+/doc/development/contributing/verify/ @gitlab-org/maintainers/cicd-verify
+/doc/development/database/ @abdwdd @alexpooley @manojmj
+/doc/development/distribution/ @gitlab-org/distribution
/doc/development/documentation/ @sselhorn
-/doc/development/export_csv.md @eread @ashrafkhamis
-/doc/development/fe_guide/customizable_dashboards.md @lciutacu
-/doc/development/fe_guide/merge_request_widget_extensions.md @aqualls
-/doc/development/fe_guide/onboarding_course/ @sselhorn
-/doc/development/fe_guide/source_editor.md @msedlakjakubowski
-/doc/development/fe_guide/view_component.md @sselhorn
-/doc/development/feature_categorization/ @sselhorn
-/doc/development/fips_compliance.md @msedlakjakubowski
-/doc/development/geo.md @axil
-/doc/development/geo/ @axil
-/doc/development/git_object_deduplication.md @eread
-/doc/development/gitaly.md @eread
-/doc/development/gitlab_flavored_markdown/ @ashrafkhamis
-/doc/development/gitlab_shell/ @msedlakjakubowski
-/doc/development/graphql_guide/ @eread @ashrafkhamis
-/doc/development/graphql_guide/batchloader.md @aqualls
-/doc/development/i18n/ @eread @ashrafkhamis
-/doc/development/identity_verification.md @phillipwells
-/doc/development/image_scaling.md @lciutacu
-/doc/development/import_export.md @eread @ashrafkhamis
-/doc/development/integrations/ @eread @ashrafkhamis
-/doc/development/integrations/secure.md @rdickenson
-/doc/development/integrations/secure_partner_integration.md @rdickenson
-/doc/development/internal_analytics/ @lciutacu
-/doc/development/internal_api/ @msedlakjakubowski
-/doc/development/issuable-like-models.md @msedlakjakubowski
-/doc/development/issue_types.md @msedlakjakubowski
-/doc/development/kubernetes.md @phillipwells
-/doc/development/lfs.md @msedlakjakubowski
-/doc/development/maintenance_mode.md @axil
-/doc/development/merge_request_concepts/ @aqualls
-/doc/development/merge_request_concepts/rate_limits.md @msedlakjakubowski
-/doc/development/migration_style_guide.md @aqualls
-/doc/development/navigation_sidebar.md @sselhorn
-/doc/development/omnibus.md @axil
-/doc/development/organization/ @lciutacu
-/doc/development/packages/ @phillipwells
-/doc/development/packages/cleanup_policies.md @marcel.amirault
-/doc/development/packages/dependency_proxy.md @marcel.amirault
-/doc/development/packages/harbor_registry_development.md @marcel.amirault
-/doc/development/permissions.md @jglassman1
-/doc/development/permissions/ @jglassman1
-/doc/development/policies.md @jglassman1
-/doc/development/project_templates.md @msedlakjakubowski
-/doc/development/project_templates/ @msedlakjakubowski
-/doc/development/rails_endpoints/ @msedlakjakubowski
-/doc/development/real_time.md @jglassman1
-/doc/development/search/ @ashrafkhamis
-/doc/development/sec/ @rdickenson
-/doc/development/spam_protection_and_captcha/ @phillipwells
-/doc/development/sql.md @aqualls
-/doc/development/value_stream_analytics.md @lciutacu
-/doc/development/value_stream_analytics/ @lciutacu
-/doc/development/work_items.md @msedlakjakubowski
-/doc/development/work_items_widgets.md @msedlakjakubowski
-/doc/development/workhorse/ @msedlakjakubowski
+/doc/development/fe_guide/customizable_dashboards.md @gitlab-org/analytics-section/product-analytics/engineers/frontend
+/doc/development/fe_guide/onboarding_course/ @gitlab-org/manage/foundations/engineering
+/doc/development/fe_guide/view_component.md @gitlab-org/manage/foundations/engineering
+/doc/development/git_object_deduplication.md @proglottis @toon
+/doc/development/gitaly.md @proglottis @toon
+/doc/development/gitlab_flavored_markdown/ @gitlab-org/maintainers/remote-development/backend @gitlab-org/maintainers/remote-development/frontend
+/doc/development/gitpod_internals.md @gl-quality/eng-prod
+/doc/development/image_scaling.md @abdwdd @alexpooley @manojmj
+/doc/development/internal_analytics/ @gitlab-org/analytics-section/product-analytics/engineers/frontend @gitlab-org/analytics-section/analytics-instrumentation/engineers
+/doc/development/navigation_sidebar.md @gitlab-org/manage/foundations/engineering
+/doc/development/omnibus.md @gitlab-org/distribution
+/doc/development/organization/ @abdwdd @alexpooley @manojmj
+/doc/development/permissions.md @gitlab-org/govern/authentication/approvers
+/doc/development/permissions/ @gitlab-org/govern/authentication/approvers
+/doc/development/permissions/custom_roles.md @gitlab-org/govern/authorization/approvers
+/doc/development/pipelines/ @gl-quality/eng-prod
+/doc/development/policies.md @gitlab-org/govern/authentication/approvers
+/doc/development/search/ @gitlab-org/search-team/migration-maintainers
+/doc/development/sec/ @gitlab-org/govern/threat-insights-frontend-team
+/doc/development/sec/gemnasium_analyzer_data.md @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis
+/doc/development/software_design.md @gl-quality/eng-prod
/doc/downgrade_ee_to_ce/ @axil
/doc/drawers/ @ashrafkhamis
/doc/editor_extensions/ @aqualls
@@ -873,6 +823,7 @@ lib/gitlab/checks/**
/doc/security/ @jglassman1
/doc/security/email_verification.md @phillipwells
/doc/security/identity_verification.md @phillipwells
+/doc/solutions/ @jfullam @brianwald @Darwinjs
/doc/subscriptions/ @fneill
/doc/subscriptions/gitlab_dedicated/ @lyspin
/doc/topics/autodevops/ @phillipwells
diff --git a/app/assets/javascripts/admin/users/components/app.vue b/app/assets/javascripts/admin/users/components/app.vue
index a3abd904a6b..b0caffb6ca6 100644
--- a/app/assets/javascripts/admin/users/components/app.vue
+++ b/app/assets/javascripts/admin/users/components/app.vue
@@ -1,9 +1,15 @@
<script>
-import UsersTable from './users_table.vue';
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
+import UserActions from './user_actions.vue';
export default {
components: {
UsersTable,
+ UserActions,
},
props: {
users: {
@@ -16,11 +22,64 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: {},
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createAlert({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ computed: {
+ groupCountsLoading() {
+ return this.$apollo.queries.groupCounts.loading;
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
};
</script>
<template>
<div>
- <users-table :users="users" :paths="paths" />
+ <users-table
+ :users="users"
+ :admin-user-path="paths.adminUser"
+ :group-counts="groupCounts"
+ :group-counts-loading="groupCountsLoading"
+ >
+ <template #user-actions="{ user }">
+ <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ </template>
+ </users-table>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 43c9a8749cd..73383623aa2 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1,9 +1,5 @@
import { s__, __ } from '~/locale';
-export const USER_AVATAR_SIZE = 32;
-
-export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
-
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
userAdministration: s__('AdminUsers|User administration'),
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index 658a94e6af4..d36701323da 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -17,7 +17,8 @@ export default {
},
isClosed: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
path: {
type: String,
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 119f8259be7..e0708289b43 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -307,7 +307,7 @@ export default {
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
/>
- <log :job-log="jobLog" :is-complete="isJobLogComplete" :search-results="searchResults" />
+ <log :search-results="searchResults" />
</div>
<!-- EO job log -->
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index fa23589f7d6..6f538e3b3d4 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -175,7 +175,7 @@ export const fetchJobLog = ({ dispatch, state }) =>
}
})
.catch((e) => {
- if (e.response.status === HTTP_STATUS_FORBIDDEN) {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index b18a3fa162d..c8b33638821 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -117,28 +117,31 @@ export const getNextLineNumber = (acc) => {
* @returns Array parsed log lines
*/
export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
- lines.reduce((acc, line) => {
- const lineNumber = getNextLineNumber(acc);
-
- const last = acc[acc.length - 1];
-
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
- }
+ lines.reduce(
+ (acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // If the object is an header, we parse it into another structure
+ if (line.section_header) {
+ acc.push(parseHeaderLine(line, lineNumber, hash));
+ } else if (isCollapsibleSection(acc, last, line)) {
+ // if the object belongs to a nested section, we append it to the new `lines` array of the
+ // previously formatted header
+ last.lines.push(parseLine(line, lineNumber));
+ } else if (line.section_duration) {
+ // if the line has section_duration, we look for the correct header to add it
+ addDurationToHeader(acc, line);
+ } else {
+ // otherwise it's a regular line
+ acc.push(parseLine(line, lineNumber));
+ }
- return acc;
- }, prevLogLines);
+ return acc;
+ },
+ [...prevLogLines],
+ );
/**
* Finds the repeated offset, removes the old one
diff --git a/app/assets/javascripts/vue_shared/components/users_table/constants.js b/app/assets/javascripts/vue_shared/components/users_table/constants.js
new file mode 100644
index 00000000000..2a063a1be33
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/constants.js
@@ -0,0 +1,3 @@
+export const USER_AVATAR_SIZE = 32;
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
index dd354794cf3..5d86f90880d 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -1,7 +1,7 @@
<script>
import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { truncate } from '~/lib/utils/text_utility';
-import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
export default {
directives: {
@@ -23,12 +23,21 @@ export default {
},
},
computed: {
+ subLabel() {
+ if (this.user.email) {
+ return {
+ label: this.user.email,
+ link: `mailto:${this.user.email}`,
+ };
+ }
+
+ return {
+ label: `@${this.user.username}`,
+ };
+ },
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
- adminUserMailto() {
- return `mailto:${this.user.email}`;
- },
userNoteShort() {
return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
},
@@ -48,9 +57,9 @@ export default {
:size="$options.USER_AVATAR_SIZE"
:src="user.avatarUrl"
:label="user.name"
- :sub-label="user.email"
+ :sub-label="subLabel.label"
:label-link="adminUserHref"
- :sub-label-link="adminUserMailto"
+ :sub-label-link="subLabel.link"
>
<template #meta>
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
index 65737be1e67..be164bb07a3 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
@@ -1,12 +1,8 @@
<script>
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { thWidthPercent } from '~/lib/utils/table_utility';
-import { s__, __ } from '~/locale';
+import { __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
-import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
-import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
export default {
@@ -14,7 +10,6 @@ export default {
GlSkeletonLoader,
GlTable,
UserAvatar,
- UserActions,
UserDate,
},
props: {
@@ -22,49 +17,20 @@ export default {
type: Array,
required: true,
},
- paths: {
- type: Object,
+ adminUserPath: {
+ type: String,
required: true,
},
- },
- data() {
- return {
- groupCounts: [],
- };
- },
- apollo: {
groupCounts: {
- query: getUsersGroupCountsQuery,
- variables() {
- return {
- usernames: this.users.map((user) => user.username),
- };
- },
- update(data) {
- const nodes = data?.users?.nodes || [];
- const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
-
- return parsedIds.reduce((acc, { id, groupCount }) => {
- acc[id] = groupCount || 0;
- return acc;
- }, {});
- },
- error(error) {
- createAlert({
- message: this.$options.i18n.groupCountFetchError,
- captureError: true,
- error,
- });
- },
- skip() {
- return !this.users.length;
- },
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ groupCountsLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- },
- i18n: {
- groupCountFetchError: s__(
- 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
- ),
},
fields: [
{
@@ -112,7 +78,7 @@ export default {
:tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
>
<template #cell(name)="{ item: user }">
- <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="adminUserPath" />
</template>
<template #cell(createdAt)="{ item: { createdAt } }">
@@ -125,17 +91,19 @@ export default {
<template #cell(groupCount)="{ item: { id } }">
<div :data-testid="`user-group-count-${id}`">
- <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
- <span v-else>{{ groupCounts[id] }}</span>
+ <gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] || 0 }}</span>
</div>
</template>
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
- <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
+ <div :data-testid="`user-project-count-${id}`">
+ {{ projectsCount || 0 }}
+ </div>
</template>
<template #cell(settings)="{ item: user }">
- <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ <slot name="user-actions" :user="user"></slot>
</template>
</gl-table>
</div>
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 5a419aab8e1..d5a7f25d4ce 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -15,6 +15,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build_report_results!, only: [:test_report_summary]
before_action :authorize_update_build!,
except: [:index, :show, :raw, :trace, :erase, :cancel, :unschedule, :test_report_summary]
+ before_action :authorize_cancel_build!, only: [:cancel]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
@@ -193,6 +194,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, @build)
end
+ def authorize_cancel_build!
+ return access_denied! unless can?(current_user, :cancel_build, @build)
+ end
+
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, @build)
end
diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb
index cfd68380005..94adbf7c59b 100644
--- a/app/graphql/types/permission_types/ci/pipeline.rb
+++ b/app/graphql/types/permission_types/ci/pipeline.rb
@@ -8,6 +8,7 @@ module Types
abilities :admin_pipeline, :destroy_pipeline
ability_field :update_pipeline, calls_gitaly: true
+ ability_field :cancel_pipeline, calls_gitaly: true
end
end
end
diff --git a/app/serializers/ci/job_entity.rb b/app/serializers/ci/job_entity.rb
index 813938c2a18..828a9eb33a5 100644
--- a/app/serializers/ci/job_entity.rb
+++ b/app/serializers/ci/job_entity.rb
@@ -53,7 +53,7 @@ module Ci
alias_method :job, :object
def cancelable?
- job.cancelable? && can?(request.current_user, :update_build, job)
+ job.cancelable? && can?(request.current_user, :cancel_build, job)
end
def retryable?
diff --git a/app/services/ci/catalog/resources/release_service.rb b/app/services/ci/catalog/resources/release_service.rb
new file mode 100644
index 00000000000..ad77bff3ef9
--- /dev/null
+++ b/app/services/ci/catalog/resources/release_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ module Resources
+ class ReleaseService
+ def initialize(release)
+ @release = release
+ @project = release.project
+ @errors = []
+ end
+
+ def execute
+ validate_catalog_resource
+ create_version
+
+ if errors.empty?
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: errors.join(', '))
+ end
+ end
+
+ private
+
+ attr_reader :project, :errors, :release
+
+ def validate_catalog_resource
+ response = Ci::Catalog::Resources::ValidateService.new(project, release.sha).execute
+ return if response.success?
+
+ errors << response.message
+ end
+
+ def create_version
+ return if errors.present?
+
+ response = Ci::Catalog::Resources::Versions::CreateService.new(release).execute
+ return if response.success?
+
+ errors << response.message
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index d7c3e9e7f64..a8ea5ac6df0 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -39,10 +39,6 @@ module Ci
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
- if Feature.disabled?(:create_deployment_only_for_processable_jobs, project)
- ::Deployments::CreateForJobService.new.execute(new_job)
- end
-
::MergeRequests::AddTodoWhenBuildFailsService
.new(project: project)
.close(new_job)
diff --git a/app/services/personal_access_tokens/rotate_service.rb b/app/services/personal_access_tokens/rotate_service.rb
index b765aacef68..32710629caf 100644
--- a/app/services/personal_access_tokens/rotate_service.rb
+++ b/app/services/personal_access_tokens/rotate_service.rb
@@ -9,7 +9,7 @@ module PersonalAccessTokens
@token = token
end
- def execute
+ def execute(params = {})
return ServiceResponse.error(message: _('token already revoked')) if token.revoked?
response = ServiceResponse.success
@@ -21,7 +21,7 @@ module PersonalAccessTokens
end
target_user = token.user
- new_token = target_user.personal_access_tokens.create(create_token_params(token))
+ new_token = target_user.personal_access_tokens.create(create_token_params(token, params))
if new_token.persisted?
response = ServiceResponse.success(payload: { personal_access_token: new_token })
@@ -39,12 +39,13 @@ module PersonalAccessTokens
attr_reader :current_user, :token
- def create_token_params(token)
+ def create_token_params(token, params)
+ expires_at = params[:expires_at] || (Date.today + EXPIRATION_PERIOD)
{ name: token.name,
previous_personal_access_token_id: token.id,
impersonation: token.impersonation,
scopes: token.scopes,
- expires_at: Date.today + EXPIRATION_PERIOD }
+ expires_at: expires_at }
end
end
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 034cb66c8b9..0e105ca3575 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -18,12 +18,6 @@ module Releases
return tag unless tag.is_a?(Gitlab::Git::Tag)
- if project.catalog_resource
- response = Ci::Catalog::Resources::ValidateService.new(project, ref).execute
-
- return error(response.message) if response.error?
- end
-
create_release(tag, evidence_pipeline)
end
@@ -56,6 +50,12 @@ module Releases
def create_release(tag, evidence_pipeline)
release = build_release(tag)
+ if project.catalog_resource && release.valid?
+ response = Ci::Catalog::Resources::ReleaseService.new(release).execute
+
+ return error(response.message) if response.error?
+ end
+
release.save!
notify_create_release(release)
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 366a51ef29e..dcc239a2700 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -3,7 +3,7 @@
!!! 5
%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
= render "layouts/head", { startup_filename: 'signin' }
- %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } }
+ %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page', testid: 'login-page' } }
= header_message
= render "layouts/init_client_detection_flags"
- if Feature.enabled?(:restyle_login_page, @project)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 6ec9b4a233d..76d6b0a042d 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -104,10 +104,10 @@
.btn-group
- if can?(current_user, :read_job_artifacts, job) && job.artifacts?
= link_button_to nil, download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), icon: 'download'
+ - if can?(current_user, :cancel_build, job) && job.active?
+ = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- if can?(current_user, :update_build, job)
- - if job.active? && can?(current_user, :cancel_build, job)
- = link_button_to nil, cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), icon: 'cancel'
- - elsif job.scheduled?
+ - if job.scheduled?
= render Pajamas::ButtonComponent.new(disabled: true, icon: 'planning') do
%time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
= duration_in_numbers(job.execute_in)
@@ -124,7 +124,7 @@
class: 'has-tooltip',
icon: 'time-out'
- elsif allow_retry
- - if job.playable? && !admin && can?(current_user, :update_build, job)
+ - if job.playable? && !admin
= link_button_to nil, play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), icon: 'play'
- elsif job.retryable?
= link_button_to nil, retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), icon: 'retry'
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index 703cae8bf88..8d7a62e5b09 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -17,24 +17,10 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- create_deployments!(pipeline)
-
Ci::PipelineCreation::StartPipelineService
.new(pipeline)
.execute
end
end
-
- private
-
- def create_deployments!(pipeline)
- return if Feature.enabled?(:create_deployment_only_for_processable_jobs, pipeline.project)
-
- pipeline.stages.flat_map(&:statuses).each { |build| create_deployment(build) }
- end
-
- def create_deployment(build)
- ::Deployments::CreateForJobService.new.execute(build)
- end
end
end
diff --git a/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml b/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml
deleted file mode 100644
index f721dd8265c..00000000000
--- a/config/feature_flags/development/create_deployment_only_for_processable_jobs.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: create_deployment_only_for_processable_jobs
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132835
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427062
-milestone: '16.5'
-type: development
-group: group::environments
-default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 65aa3bdd5e7..98c5d13e75a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22829,6 +22829,7 @@ Represents pipeline counts for the project.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pipelinepermissionsadminpipeline"></a>`adminPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `admin_pipeline` on this resource. |
+| <a id="pipelinepermissionscancelpipeline"></a>`cancelPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `cancel_pipeline` on this resource. |
| <a id="pipelinepermissionsdestroypipeline"></a>`destroyPipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `destroy_pipeline` on this resource. |
| <a id="pipelinepermissionsupdatepipeline"></a>`updatePipeline` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_pipeline` on this resource. |
diff --git a/doc/ci/testing/browser_performance_testing.md b/doc/ci/testing/browser_performance_testing.md
index 9e81f243e50..2b6c2067c35 100644
--- a/doc/ci/testing/browser_performance_testing.md
+++ b/doc/ci/testing/browser_performance_testing.md
@@ -91,6 +91,7 @@ You can also customize the jobs with CI/CD variables:
- `SITESPEED_IMAGE`: Configure the Docker image to use for the job (default `sitespeedio/sitespeed.io`), but not the image version.
- `SITESPEED_VERSION`: Configure the version of the Docker image to use for the job (default `14.1.0`).
- `SITESPEED_OPTIONS`: Configure any additional sitespeed.io options as required (default `nil`). Refer to the [sitespeed.io documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) for more details.
+- `SITESPEED_DOCKER_OPTIONS`: Configure any additional Docker options (default `nil`). Refer to the [Docker options documentation](https://docs.docker.com/engine/reference/commandline/run/#options) for more details.
For example, you can override the number of runs sitespeed.io
makes on the given URL, and change the version:
diff --git a/doc/topics/autodevops/cicd_variables.md b/doc/topics/autodevops/cicd_variables.md
index 21d9dd0b3d3..4fa2ee10c75 100644
--- a/doc/topics/autodevops/cicd_variables.md
+++ b/doc/topics/autodevops/cicd_variables.md
@@ -31,6 +31,9 @@ Use these variables to customize and deploy your build.
| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | Used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | Used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASS_CREDENTIALS` | From GitLab 14.2, set to a non-empty value to enable forwarding of the Helm repository credentials to the chart server when the chart artifacts are on a different host than repository. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_INSECURE` | Set to a non-empty value to add a `--insecure-skip-tls-verify` argument to the Helm commands. By default, Helm uses TLS verification. |
+| `AUTO_DEVOPS_CHART_CUSTOM_ONLY` | Set to a non-empty value to use only a custom chart. By default, the latest chart is downloaded from GitLab. |
+| `AUTO_DEVOPS_CHART_VERSION` | Set the version of the deployment chart. Defaults to the latest available version. |
| `AUTO_DEVOPS_COMMON_NAME` | From GitLab 15.5, set to a valid domain name to customize the common name used for the TLS certificate. Defaults to `le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN`. Set to `false` to not set this alternative host on the Ingress. |
| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm outputs debug logs. |
| `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V<N>` | From [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image) v1.0.0, if this variable is present, a new major version of chart is forcibly deployed. For more information, see [Ignore warnings and continue deploying](upgrading_auto_deploy_dependencies.md#ignore-warnings-and-continue-deploying). |
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index e920ae5e5e1..2e6672e3ab0 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -208,11 +208,14 @@ repository or by specifying a project CI/CD variable:
file in it, Auto DevOps detects the chart and uses it instead of the
[default chart](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/tree/master/assets/auto-deploy-app).
- **Project variable** - Create a [project CI/CD variable](../../ci/variables/index.md)
- `AUTO_DEVOPS_CHART` with the URL of a custom chart. You can also create two project
+ `AUTO_DEVOPS_CHART` with the URL of a custom chart. You can also create five project
variables:
- `AUTO_DEVOPS_CHART_REPOSITORY` - The URL of a custom chart repository.
- `AUTO_DEVOPS_CHART` - The path to the chart.
+ - `AUTO_DEVOPS_CHART_REPOSITORY_INSECURE` - Set to a non-empty value to add a `--insecure-skip-tls-verify` argument to the Helm commands.
+ - `AUTO_DEVOPS_CHART_CUSTOM_ONLY` - Set to a non-empty value to use only a custom chart. By default, the latest chart is downloaded from GitLab.
+ - `AUTO_DEVOPS_CHART_VERSION` - The version of the deployment chart.
### Customize Helm chart values
diff --git a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
index 96819860a2f..5412ced3e6d 100644
--- a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md
@@ -95,7 +95,7 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**.
1. Set the variable `BASE64_GOOGLE_CREDENTIALS` to the `base64` encoded JSON file you just created.
-1. Set the variable `TF_VAR_gcp_project` to your GCP `project` name.
+1. Set the variable `TF_VAR_gcp_project` to your GCP `project` ID.
1. Set the variable `TF_VAR_agent_token` to the agent token displayed in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address displayed in the previous task.
@@ -113,6 +113,10 @@ contains other variables that you can override according to your needs:
Refer to the [Google Terraform provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) and the [Kubernetes Terraform provider](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs) documentation for further resource options.
+## Enable Kubernetes Engine API
+
+From the Google Cloud console, enable the [Kubernetes Engine API](https://console.cloud.google.com/apis/library/container.googleapis.com).
+
## Provision your cluster
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 3361f4564b2..b5123ab49dc 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -352,7 +352,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID', documentation: { example: 18 }
end
post ':id/pipelines/:pipeline_id/cancel', urgency: :low, feature_category: :continuous_integration do
- authorize! :update_pipeline, pipeline
+ authorize! :cancel_pipeline, pipeline
# TODO: inconsistent behavior: when pipeline is not cancelable we should return an error
::Ci::CancelPipelineService.new(pipeline: pipeline, current_user: current_user).execute
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 9d234ca0593..de00b66ead3 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -72,11 +72,17 @@ module API
detail 'Roates a personal access token.'
success Entities::PersonalAccessTokenWithToken
end
+ params do
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
+ end
post ':id/rotate' do
token = PersonalAccessToken.find_by_id(params[:id])
if Ability.allowed?(current_user, :manage_user_personal_access_token, token&.user)
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb
index 1ad5bc8d421..752feb1455f 100644
--- a/lib/api/resource_access_tokens.rb
+++ b/lib/api/resource_access_tokens.rb
@@ -141,6 +141,10 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
requires :token_id, type: String, desc: "The ID of the token"
+ optional :expires_at,
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
end
post ':id/access_tokens/:token_id/rotate' do
resource = find_source(source_type, params[:id])
@@ -149,7 +153,7 @@ module API
token = find_token(resource, params[:token_id]) if resource_accessible
if token
- response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute
+ response = ::PersonalAccessTokens::RotateService.new(current_user, token).execute(declared_params)
if response.success?
status :ok
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 21fc2980cdc..791b8a963e9 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -35,13 +35,15 @@ module Gitlab
end
attr_reader :offset, :sections, :segments, :current_segment,
- :section_header, :section_duration, :section_options
+ :section_header, :section_footer, :section_duration,
+ :section_options
def initialize(offset:, style:, sections: [])
@offset = offset
@segments = []
@sections = sections
@section_header = false
+ @section_footer = false
@duration = nil
@current_segment = Segment.new(style: style)
end
@@ -79,6 +81,10 @@ module Gitlab
@section_header = true
end
+ def set_as_section_footer
+ @section_footer = true
+ end
+
def set_section_duration(duration_in_seconds)
normalized_duration_in_seconds = duration_in_seconds.to_i.clamp(0, 1.year)
duration = ActiveSupport::Duration.build(normalized_duration_in_seconds)
@@ -103,6 +109,7 @@ module Gitlab
{ offset: offset, content: @segments }.tap do |result|
result[:section] = sections.last if sections.any?
result[:section_header] = true if @section_header
+ result[:section_footer] = true if @section_footer
result[:section_duration] = @section_duration if @section_duration
result[:section_options] = @section_options if @section_options
end
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index 3aec1cde1bc..6cf76fbbb51 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -49,6 +49,7 @@ module Gitlab
duration = timestamp.to_i - @open_sections[section].to_i
@current_line.set_section_duration(duration)
+ @current_line.set_as_section_footer
@open_sections.delete(section)
end
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
index c1a90955f7f..8c9e0a329dd 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: 26.1.0
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
index adc92fde5ae..3f4c0c53850 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.latest.gitlab-ci.yml
@@ -19,6 +19,7 @@ browser_performance:
SITESPEED_IMAGE: sitespeedio/sitespeed.io
SITESPEED_VERSION: latest
SITESPEED_OPTIONS: ''
+ SITESPEED_DOCKER_OPTIONS: ''
services:
- docker:dind
script:
@@ -48,7 +49,7 @@ browser_performance:
HTTP_PROXY \
NO_PROXY \
) \
- --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
+ $SITESPEED_DOCKER_OPTIONS --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS
- mv sitespeed-results/data/performance.json browser-performance.json
artifacts:
paths:
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index 7415e6c920c..de1401feb8a 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -33,20 +33,18 @@ namespace :tw do
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
- CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
CodeOwnerRule.new('Contributor Experience', '@eread'),
CodeOwnerRule.new('Database', '@aqualls'),
CodeOwnerRule.new('DataOps', '@sselhorn'),
# CodeOwnerRule.new('Delivery', ''),
- CodeOwnerRule.new('Development', '@sselhorn'),
CodeOwnerRule.new('Distribution', '@axil'),
CodeOwnerRule.new('Distribution (Charts)', '@axil'),
CodeOwnerRule.new('Distribution (Omnibus)', '@eread'),
- CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Duo Chat', '@sselhorn'),
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'),
CodeOwnerRule.new('Editor Extensions', '@aqualls'),
+ CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Foundations', '@sselhorn'),
# CodeOwnerRule.new('Fulfillment Platform', ''),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
@@ -79,7 +77,6 @@ namespace :tw do
CodeOwnerRule.new('Solutions Architecture', '@jfullam @brianwald @Darwinjs'),
CodeOwnerRule.new('Source Code', '@msedlakjakubowski'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
- CodeOwnerRule.new('Style Guide', '@sselhorn'),
CodeOwnerRule.new('Tenant Scale', '@lciutacu'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Threat Insights', '@rdickenson'),
@@ -89,6 +86,33 @@ namespace :tw do
# CodeOwnerRule.new('Vulnerability Research', '')
].freeze
+ CONTRIBUTOR_DOCS_PATH = '/doc/development/'
+ CONTRIBUTOR_DOCS_CODE_OWNER_RULES = [
+ CodeOwnerRule.new('Analytics Instrumentation',
+ '@gitlab-org/analytics-section/product-analytics/engineers/frontend ' \
+ '@gitlab-org/analytics-section/analytics-instrumentation/engineers'),
+ CodeOwnerRule.new('Authentication', '@gitlab-org/govern/authentication/approvers'),
+ CodeOwnerRule.new('Authorization', '@gitlab-org/govern/authorization/approvers'),
+ CodeOwnerRule.new('Compliance',
+ '@gitlab-org/govern/security-policies-frontend @gitlab-org/govern/threat-insights-frontend-team ' \
+ '@gitlab-org/govern/threat-insights-backend-team'),
+ CodeOwnerRule.new('Composition Analysis',
+ '@gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis'),
+ CodeOwnerRule.new('Distribution', '@gitlab-org/distribution'),
+ CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
+ CodeOwnerRule.new('Engineering Productivity', '@gl-quality/eng-prod'),
+ CodeOwnerRule.new('Foundations', '@gitlab-org/manage/foundations/engineering'),
+ CodeOwnerRule.new('Gitaly', '@proglottis @toon'),
+ CodeOwnerRule.new('Global Search', '@gitlab-org/search-team/migration-maintainers'),
+ CodeOwnerRule.new('IDE',
+ '@gitlab-org/maintainers/remote-development/backend @gitlab-org/maintainers/remote-development/frontend'),
+ CodeOwnerRule.new('Pipeline Authoring', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Pipeline Execution', '@gitlab-org/maintainers/cicd-verify'),
+ CodeOwnerRule.new('Product Analytics', '@gitlab-org/analytics-section/product-analytics/engineers/frontend'),
+ CodeOwnerRule.new('Tenant Scale', '@abdwdd @alexpooley @manojmj'),
+ CodeOwnerRule.new('Threat Insights', '@gitlab-org/govern/threat-insights-frontend-team')
+ ].freeze
+
ERRORS_EXCLUDED_FILES = [
'/doc/architecture'
].freeze
@@ -107,7 +131,8 @@ namespace :tw do
end
def self.writer_for_group(category, path)
- writer = CODE_OWNER_RULES.find { |rule| rule.category == category }&.writer
+ rules = path.start_with?(CONTRIBUTOR_DOCS_PATH) ? CONTRIBUTOR_DOCS_CODE_OWNER_RULES : CODE_OWNER_RULES
+ writer = rules.find { |rule| rule.category == category }&.writer
if writer.is_a?(String) || writer.nil?
writer
diff --git a/qa/qa/page/admin/overview/users/index.rb b/qa/qa/page/admin/overview/users/index.rb
index c444b728f5a..fb1a7c29008 100644
--- a/qa/qa/page/admin/overview/users/index.rb
+++ b/qa/qa/page/admin/overview/users/index.rb
@@ -11,7 +11,7 @@ module QA
element 'pending-approval-tab'
end
- view 'app/assets/javascripts/admin/users/components/users_table.vue' do
+ view 'app/assets/javascripts/vue_shared/components/users_table/users_table.vue' do
element 'user-row-content'
end
diff --git a/qa/qa/runtime/path.rb b/qa/qa/runtime/path.rb
index ae1b26ca84a..d122240225c 100644
--- a/qa/qa/runtime/path.rb
+++ b/qa/qa/runtime/path.rb
@@ -15,6 +15,10 @@ module QA
def fixture(*args)
::File.join(fixtures_path, *args)
end
+
+ def qa_tmp(*args)
+ ::File.join([qa_root, 'tmp', *args].compact)
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb
index 3bd7912958f..bcddfc4b3a9 100644
--- a/qa/qa/service/docker_run/base.rb
+++ b/qa/qa/service/docker_run/base.rb
@@ -120,6 +120,14 @@ module QA
# If the host could not be resolved, fallback on localhost
'127.0.0.1'
end
+
+ # Copy files to/from the Docker container and the host
+ #
+ # @param from the source path to copy files from
+ # @param to the destination path to copy files to
+ def copy(from:, to:)
+ shell("docker cp #{from} #{to}")
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/gitlab.rb b/qa/qa/service/docker_run/gitlab.rb
index ce8ab17f2b5..c39f4c22865 100644
--- a/qa/qa/service/docker_run/gitlab.rb
+++ b/qa/qa/service/docker_run/gitlab.rb
@@ -32,6 +32,11 @@ module QA
CMD
end
+ # Copy logs for GitLab services from the Docker container to the test framework's tmp folder
+ def extract_service_logs
+ copy(from: "#{@name}:/var/log/gitlab", to: Runtime::Path.qa_tmp(@name))
+ end
+
private
def release_variables_available?
diff --git a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
index 7ac34d86b62..6d5a2aef76c 100644
--- a/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
+++ b/qa/qa/specs/features/browser_ui/10_govern/login/login_via_oauth_and_oidc_with_gitlab_as_idp_spec.rb
@@ -14,6 +14,7 @@ module QA
after do
instance_oauth_app.remove_via_api!
+ save_gitlab_logs(consumer_name)
remove_gitlab_service(consumer_name)
end
@@ -28,6 +29,11 @@ module QA
end
end
+ # Copy GitLab logs from inside the named Docker container running the GitLab OAuth instance
+ def save_gitlab_logs(name)
+ Service::DockerRun::Gitlab.new(name: name).extract_service_logs
+ end
+
def remove_gitlab_service(name)
Service::DockerRun::Gitlab.new(name: name).remove!
end
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index d40089edc82..4b224947303 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -1,14 +1,42 @@
-import { shallowMount } from '@vue/test-utils';
-
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import AdminUsersApp from '~/admin/users/components/app.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import { users, paths } from '../mock_data';
+import UserActions from '~/admin/users/components/user_actions.vue';
+import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import { createAlert } from '~/alert';
+import { users, paths, createGroupCountResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
describe('AdminUsersApp component', () => {
let wrapper;
+ const user = users[0];
+
+ const mockSuccessData = [{ id: user.id, groupCount: 5 }];
+ const mockParsedGroupCount = { 2177: 5 };
+ const mockError = new Error();
+
+ const createFetchGroupCount = (data) =>
+ jest.fn().mockResolvedValue(createGroupCountResponse(data));
+ const loadingResolver = jest.fn().mockResolvedValue(new Promise(() => {}));
+ const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
+ const successfulResolver = createFetchGroupCount(mockSuccessData);
- const initComponent = (props = {}) => {
- wrapper = shallowMount(AdminUsersApp, {
+ function createMockApolloProvider(resolverMock) {
+ const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ const initComponent = (props = {}, resolverMock = successfulResolver) => {
+ wrapper = mount(AdminUsersApp, {
+ apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
users,
paths,
@@ -17,16 +45,47 @@ describe('AdminUsersApp component', () => {
});
};
- describe('when initialized', () => {
- beforeEach(() => {
+ const findUsersTable = () => wrapper.findComponent(UsersTable);
+ const findAllUserActions = () => wrapper.findAllComponents(UserActions);
+
+ describe.each`
+ description | mockResolver | loading | groupCounts | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${{}} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver} | ${false} | ${mockParsedGroupCount} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${{}} | ${true}
+ `('$description', ({ mockResolver, loading, groupCounts, error }) => {
+ beforeEach(async () => {
+ initComponent({}, mockResolver);
+ await waitForPromises();
+ });
+
+ it(`renders the UsersTable with group-counts-loading set to ${loading}`, () => {
+ expect(findUsersTable().props('groupCountsLoading')).toBe(loading);
+ });
+
+ it('renders the UsersTable with the correct group-counts data', () => {
+ expect(findUsersTable().props('groupCounts')).toStrictEqual(groupCounts);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({
+ message: 'Could not load user group counts. Please refresh the page to try again.',
+ error: mockError,
+ captureError: true,
+ })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('UserActions', () => {
+ beforeEach(async () => {
initComponent();
+ await waitForPromises();
});
- it('renders the admin users table with props', () => {
- expect(wrapper.findComponent(AdminUsersTable).props()).toEqual({
- users,
- paths,
- });
+ it('renders a UserActions component for each user', () => {
+ expect(findAllUserActions().wrappers.map((w) => w.props('user'))).toStrictEqual(users);
});
});
});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
deleted file mode 100644
index 6f658fd2e59..00000000000
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-
-import AdminUserActions from '~/admin/users/components/user_actions.vue';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
-import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import { createAlert } from '~/alert';
-import AdminUserDate from '~/vue_shared/components/user_date.vue';
-
-import { users, paths, createGroupCountResponse } from '../mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-
-describe('AdminUsersTable component', () => {
- let wrapper;
- const user = users[0];
-
- const createFetchGroupCount = (data) =>
- jest.fn().mockResolvedValue(createGroupCountResponse(data));
- const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
- const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error'));
- const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
-
- const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
- const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
- const getCellByLabel = (trIdx, label) => {
- return wrapper
- .findComponent(GlTable)
- .find('tbody')
- .findAll('tr')
- .at(trIdx)
- .find(`[data-label="${label}"][role="cell"]`);
- };
-
- function createMockApolloProvider(resolverMock) {
- const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]];
-
- return createMockApollo(requestHandlers);
- }
-
- const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => {
- wrapper = mountExtended(AdminUsersTable, {
- apolloProvider: createMockApolloProvider(resolverMock),
- propsData: {
- users,
- paths,
- ...props,
- },
- });
- };
-
- describe('when there are users', () => {
- beforeEach(() => {
- initComponent();
- });
-
- it('renders the projects count', () => {
- expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
- });
-
- it('renders the user actions', () => {
- expect(wrapper.findComponent(AdminUserActions).exists()).toBe(true);
- });
-
- it.each`
- component | label
- ${AdminUserAvatar} | ${'Name'}
- ${AdminUserDate} | ${'Created on'}
- ${AdminUserDate} | ${'Last activity'}
- `('renders the component for column $label', ({ component, label }) => {
- expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
- });
- });
-
- describe('when users is an empty array', () => {
- beforeEach(() => {
- initComponent({ users: [] });
- });
-
- it('renders a "No users found" message', () => {
- expect(wrapper.text()).toContain('No users found');
- });
- });
-
- describe('group counts', () => {
- describe('when fetching the data', () => {
- beforeEach(() => {
- initComponent({}, fetchGroupCountsLoading);
- });
-
- it('renders a loader for each user', () => {
- expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
- });
- });
-
- describe('when the data has been fetched', () => {
- beforeEach(async () => {
- initComponent();
- await waitForPromises();
- });
-
- it("renders the user's group count", () => {
- expect(findUserGroupCount(user.id).text()).toBe('5');
- });
-
- describe("and a user's group count is null", () => {
- beforeEach(async () => {
- initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }]));
- await waitForPromises();
- });
-
- it("renders the user's group count as 0", () => {
- expect(findUserGroupCount(user.id).text()).toBe('0');
- });
- });
- });
-
- describe('when there is an error while fetching the data', () => {
- beforeEach(async () => {
- initComponent({}, fetchGroupCountsError);
- await waitForPromises();
- });
-
- it('creates an alert message and captures the error', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Could not load user group counts. Please refresh the page to try again.',
- captureError: true,
- error: expect.any(Error),
- });
- });
- });
- });
-});
diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index 45296e4b6c2..c75f5fa30d5 100644
--- a/spec/frontend/ci/job_details/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -30,6 +31,8 @@ describe('Job Log Header Line', () => {
});
};
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
describe('line', () => {
beforeEach(() => {
createComponent();
@@ -48,23 +51,33 @@ describe('Job Log Header Line', () => {
});
});
- describe('when isCloses is true', () => {
+ describe('when isClosed is true', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: true });
});
it('sets icon name to be chevron-lg-right', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-right');
+ expect(findIcon().props('name')).toEqual('chevron-lg-right');
});
});
- describe('when isCloses is false', () => {
+ describe('when isClosed is false', () => {
beforeEach(() => {
createComponent({ ...defaultProps, isClosed: false });
});
it('sets icon name to be chevron-lg-down', () => {
- expect(wrapper.vm.iconName).toEqual('chevron-lg-down');
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
+ });
+ });
+
+ describe('when isClosed is not defined', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultProps, isClosed: undefined });
+ });
+
+ it('sets icon name to be chevron-lg-right', () => {
+ expect(findIcon().props('name')).toEqual('chevron-lg-down');
});
});
diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index 14669872cc1..d9b1354f475 100644
--- a/spec/frontend/ci/job_details/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
@@ -1,67 +1,73 @@
-export const mockJobLog = [
+export const mockJobLines = [
{
- offset: 1000,
- content: [{ text: 'Running with gitlab-runner 12.1.0 (de7731dd)' }],
+ offset: 0,
+ content: [
+ {
+ text: 'Running with gitlab-runner 12.1.0 (de7731dd)',
+ style: 'term-fg-l-cyan term-bold',
+ },
+ ],
},
{
offset: 1001,
content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
},
+];
+
+export const mockEmptySection = [
{
offset: 1002,
content: [
{
- text: 'Using Docker executor with image dev.gitlab.org3',
+ text: 'Resolving secrets',
+ style: 'term-fg-l-cyan term-bold',
},
],
- section: 'prepare-executor',
+ section: 'resolve-secrets',
section_header: true,
},
{
offset: 1003,
- content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
- section: 'prepare-executor',
- },
- {
- offset: 1004,
- content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
- section: 'prepare-executor',
- },
- {
- offset: 1005,
content: [],
- section: 'prepare-executor',
- section_duration: '00:09',
+ section: 'resolve-secrets',
+ section_footer: true,
+ section_duration: '00:00',
},
+];
+
+export const mockContentSection = [
{
- offset: 1006,
+ offset: 1004,
content: [
{
- text: 'Getting source from Git repository',
+ text: 'Using Docker executor with image dev.gitlab.org3',
},
],
- section: 'get-sources',
+ section: 'prepare-executor',
section_header: true,
},
{
- offset: 1007,
- content: [{ text: 'Fetching changes with git depth set to 20...' }],
- section: 'get-sources',
+ offset: 1005,
+ content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
+ section: 'prepare-executor',
},
{
- offset: 1008,
- content: [{ text: 'Initialized empty Git repository', style: 'term-fg-l-green' }],
- section: 'get-sources',
+ offset: 1006,
+ content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
+ section: 'prepare-executor',
},
{
- offset: 1009,
+ offset: 1007,
content: [],
- section: 'get-sources',
- section_duration: '00:19',
+ section: 'prepare-executor',
+ section_footer: true,
+ section_duration: '00:09',
},
];
-export const mockJobLogLineCount = 8; // `text` entries in mockJobLog
+export const mockJobLog = [...mockJobLines, ...mockEmptySection, ...mockContentSection];
+
+export const mockJobLogLineCount = 6; // `text` entries in mockJobLog
export const originalTrace = [
{
diff --git a/spec/frontend/ci/job_details/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index ff84b2d0283..2bd0429ef56 100644
--- a/spec/frontend/ci/job_details/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -311,6 +311,8 @@ describe('Job App', () => {
it('should render job log', () => {
expect(findJobLog().exists()).toBe(true);
+
+ expect(findJobLog().props()).toEqual({ searchResults: [] });
});
});
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index 2799bc9578c..849f55ac444 100644
--- a/spec/frontend/ci/job_details/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -284,7 +284,7 @@ describe('Job State actions', () => {
});
});
- describe('error', () => {
+ describe('server error', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
@@ -303,6 +303,28 @@ describe('Job State actions', () => {
);
});
});
+
+ describe('unexpected error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(() => {
+ throw new Error('an error');
+ });
+ });
+
+ it('dispatches requestJobLog and receiveJobLogError', () => {
+ return testAction(
+ fetchJobLog,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'receiveJobLogError',
+ },
+ ],
+ );
+ });
+ });
});
describe('startPollingJobLog', () => {
diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 78b29efed68..601dff47584 100644
--- a/spec/frontend/ci/job_details/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -1,6 +1,7 @@
import * as types from '~/ci/job_details/store/mutation_types';
import mutations from '~/ci/job_details/store/mutations';
import state from '~/ci/job_details/store/state';
+import * as utils from '~/ci/job_details/store/utils';
describe('Jobs Store Mutations', () => {
let stateCopy;
@@ -87,50 +88,91 @@ describe('Jobs Store Mutations', () => {
});
describe('with new job log', () => {
+ const mockLog = {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils, 'logLinesParser');
+ });
+
+ afterEach(() => {
+ utils.logLinesParser.mockRestore();
+ });
+
describe('log.lines', () => {
- describe('when append is true', () => {
+ describe('when it is defined', () => {
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: true,
- size: 511846,
- complete: true,
- lines: [
- {
- offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
- },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '');
expect(stateCopy.jobLog).toEqual([
{
offset: 1,
- content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
});
- describe('when it is defined', () => {
+ describe('when it is defined and location.hash is set', () => {
+ beforeEach(() => {
+ window.location.hash = '#L1';
+ });
+
it('sets the parsed log', () => {
- mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- append: false,
- size: 511846,
- complete: true,
- lines: [
- { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
- ],
- });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, mockLog);
+
+ expect(utils.logLinesParser).toHaveBeenCalledWith(mockLog.lines, [], '#L1');
expect(stateCopy.jobLog).toEqual([
{
- offset: 0,
- content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ offset: 1,
+ content: [{ text: 'Line content' }],
lineNumber: 1,
},
]);
});
+
+ describe('when append is true', () => {
+ it('sets the parsed log', () => {
+ stateCopy.jobLog = [
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ ];
+
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
+ ...mockLog,
+ append: true,
+ });
+
+ expect(stateCopy.jobLog).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Previous line content' }],
+ lineNumber: 1,
+ },
+ {
+ offset: 1,
+ content: [{ text: 'Line content' }],
+ lineNumber: 2,
+ },
+ ]);
+ });
+ });
});
describe('when it is null', () => {
diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 394ce0ab737..8fc4eeb0ca8 100644
--- a/spec/frontend/ci/job_details/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -195,11 +195,9 @@ describe('Jobs Store Utils', () => {
expect(result[0].lineNumber).toEqual(1);
expect(result[1].lineNumber).toEqual(2);
expect(result[2].line.lineNumber).toEqual(3);
- expect(result[2].lines[0].lineNumber).toEqual(4);
- expect(result[2].lines[1].lineNumber).toEqual(5);
- expect(result[3].line.lineNumber).toEqual(6);
- expect(result[3].lines[0].lineNumber).toEqual(7);
- expect(result[3].lines[1].lineNumber).toEqual(8);
+ expect(result[3].line.lineNumber).toEqual(4);
+ expect(result[3].lines[0].lineNumber).toEqual(5);
+ expect(result[3].lines[1].lineNumber).toEqual(6);
});
});
@@ -215,16 +213,16 @@ describe('Jobs Store Utils', () => {
});
it('creates a lines array property with the content of the collapsible section', () => {
- expect(result[2].lines.length).toEqual(2);
- expect(result[2].lines[0].content).toEqual(mockJobLog[3].content);
- expect(result[2].lines[1].content).toEqual(mockJobLog[4].content);
+ expect(result[3].lines.length).toEqual(2);
+ expect(result[3].lines[0].content).toEqual(mockJobLog[5].content);
+ expect(result[3].lines[1].content).toEqual(mockJobLog[6].content);
});
});
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[2].line.section_duration).toEqual(mockJobLog[5].section_duration);
- expect(result[3].line.section_duration).toEqual(mockJobLog[9].section_duration);
+ expect(result[2].line.section_duration).toEqual(mockJobLog[3].section_duration);
+ expect(result[3].line.section_duration).toEqual(mockJobLog[7].section_duration);
});
it('does not add section duration as a line', () => {
diff --git a/spec/frontend/vue_shared/components/users_table/mock_data.js b/spec/frontend/vue_shared/components/users_table/mock_data.js
new file mode 100644
index 00000000000..c763ca2ca9b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/mock_data.js
@@ -0,0 +1,23 @@
+export const MOCK_USERS = [
+ {
+ id: 2177,
+ name: 'Nikki',
+ createdAt: '2020-11-13T12:26:54.177Z',
+ email: 'nikki@example.com',
+ username: 'nikki',
+ lastActivityOn: '2020-12-09',
+ avatarUrl:
+ 'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
+ badges: [
+ { text: 'Admin', variant: 'success' },
+ { text: "It's you!", variant: 'muted' },
+ ],
+ projectsCount: 0,
+ actions: [],
+ note: 'Create per issue #999',
+ },
+];
+
+export const MOCK_ADMIN_USER_PATH = 'admin/users/:id';
+
+export const MOCK_GROUP_COUNTS = { 2177: 5 };
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
index 02e648d2b77..035778530af 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/users_table/user_avatar_spec.js
@@ -2,15 +2,14 @@ import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
-import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants';
+import AdminUserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/vue_shared/components/users_table/constants';
import { truncate } from '~/lib/utils/text_utility';
-import { users, paths } from '../mock_data';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH } from './mock_data';
describe('AdminUserAvatar component', () => {
let wrapper;
- const user = users[0];
- const adminUserPath = paths.adminUser;
+ const user = MOCK_USERS[0];
const findNote = () => wrapper.findComponent(GlIcon);
const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
@@ -22,7 +21,7 @@ describe('AdminUserAvatar component', () => {
wrapper = shallowMount(AdminUserAvatar, {
propsData: {
user,
- adminUserPath,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
...props,
},
directives: {
@@ -50,14 +49,7 @@ describe('AdminUserAvatar component', () => {
const avatar = findAvatar();
expect(avatar.props('label')).toBe(user.name);
- expect(avatar.props('labelLink')).toBe(adminUserPath.replace('id', user.username));
- });
-
- it("renders the user's email with a mailto link", () => {
- const avatar = findAvatar();
-
- expect(avatar.props('subLabel')).toBe(user.email);
- expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
+ expect(avatar.props('labelLink')).toBe(MOCK_ADMIN_USER_PATH.replace('id', user.username));
});
it("renders the user's avatar image", () => {
@@ -118,4 +110,30 @@ describe('AdminUserAvatar component', () => {
});
});
});
+
+ describe('when user has an email address', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's email with a mailto link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(user.email);
+ expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
+ });
+ });
+
+ describe('when user does not have an email address', () => {
+ beforeEach(() => {
+ initComponent({ user: { ...MOCK_USERS[0], email: null } });
+ });
+
+ it("renders the user's username without a link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(`@${user.username}`);
+ expect(avatar.props('subLabelLink')).toBe('');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/users_table/users_table_spec.js b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
new file mode 100644
index 00000000000..45d1d291d47
--- /dev/null
+++ b/spec/frontend/vue_shared/components/users_table/users_table_spec.js
@@ -0,0 +1,95 @@
+import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
+import { MOCK_USERS, MOCK_ADMIN_USER_PATH, MOCK_GROUP_COUNTS } from './mock_data';
+
+describe('UsersTable component', () => {
+ let wrapper;
+ const user = MOCK_USERS[0];
+
+ const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
+ const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
+ const getCellByLabel = (trIdx, label) => {
+ return wrapper
+ .findComponent(GlTable)
+ .find('tbody')
+ .findAll('tr')
+ .at(trIdx)
+ .find(`[data-label="${label}"][role="cell"]`);
+ };
+
+ const initComponent = (props = {}) => {
+ wrapper = mountExtended(UsersTable, {
+ propsData: {
+ users: MOCK_USERS,
+ adminUserPath: MOCK_ADMIN_USER_PATH,
+ groupCounts: MOCK_GROUP_COUNTS,
+ groupCountsLoading: false,
+ ...props,
+ },
+ });
+ };
+
+ describe('when there are users', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
+ });
+
+ it.each`
+ component | label
+ ${UserAvatar} | ${'Name'}
+ ${UserDate} | ${'Created on'}
+ ${UserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
+ });
+ });
+
+ describe('when users is an empty array', () => {
+ beforeEach(() => {
+ initComponent({ users: [] });
+ });
+
+ it('renders a "No users found" message', () => {
+ expect(wrapper.text()).toContain('No users found');
+ });
+ });
+
+ describe('group counts', () => {
+ describe('when groupCountsLoading is true', () => {
+ beforeEach(() => {
+ initComponent({ groupCountsLoading: true });
+ });
+
+ it('renders a loader for each user', () => {
+ expect(findUserGroupCountLoader(user.id).exists()).toBe(true);
+ });
+ });
+
+ describe('when groupCounts has data', () => {
+ beforeEach(() => {
+ initComponent();
+ });
+
+ it("renders the user's group count", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('5');
+ });
+ });
+
+ describe('when groupCounts has no data', () => {
+ beforeEach(() => {
+ initComponent({ groupCounts: {} });
+ });
+
+ it("renders the user's group count as 0", () => {
+ expect(findUserGroupCount(user.id).text()).toBe('0');
+ });
+ });
+ });
+});
diff --git a/spec/graphql/types/permission_types/ci/pipeline_spec.rb b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
new file mode 100644
index 00000000000..6830b659b12
--- /dev/null
+++ b/spec/graphql/types/permission_types/ci/pipeline_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::PermissionTypes::Ci::Pipeline, feature_category: :continuous_integration do
+ it 'has expected permission fields' do
+ expected_permissions = [
+ :admin_pipeline, :destroy_pipeline, :update_pipeline, :cancel_pipeline
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions).only
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index b8563bb1d1c..475a54b275d 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Ansi2json::Line do
+RSpec.describe Gitlab::Ci::Ansi2json::Line, feature_category: :continuous_integration do
let(:offset) { 0 }
let(:style) { Gitlab::Ci::Ansi2json::Style.new }
@@ -75,6 +75,14 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
end
+ describe '#set_as_section_footer' do
+ it 'change the section_footer to true' do
+ expect { subject.set_as_section_footer }
+ .to change { subject.section_footer }
+ .to be_truthy
+ end
+ end
+
describe '#set_section_duration' do
using RSpec::Parameterized::TableSyntax
@@ -178,6 +186,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
expect(subject.to_h).to eq(result)
end
end
+
+ context 'when section footer is set' do
+ before do
+ subject.set_as_section_footer
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_footer: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
end
context 'when there are no sections' do
diff --git a/spec/lib/gitlab/ci/ansi2json/state_spec.rb b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
index 8dd4092f3d8..07e6579829a 100644
--- a/spec/lib/gitlab/ci/ansi2json/state_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/state_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
state.offset = 1
state.new_line!(style: { fg: 'some-fg', bg: 'some-bg', mask: 1234 })
state.set_last_line_offset
- state.open_section('hello', 111, {})
+ state.open_section('hello', 100, {})
end
end
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
fg: 'some-fg',
mask: 1234
})
- expect(new_state.open_sections).to eq({ 'hello' => 111 })
+ expect(new_state.open_sections).to eq({ 'hello' => 100 })
end
it 'ignores unsigned prior state', :aggregate_failures do
@@ -44,6 +44,23 @@ RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integ
expect(new_state.open_sections).to eq({})
end
+ it 'opens and closes a section', :aggregate_failures do
+ new_state = described_class.new('', 1000)
+
+ new_state.new_line!(style: {})
+ new_state.open_section('hello', 100, {})
+
+ expect(new_state.current_line.section_header).to eq(true)
+ expect(new_state.current_line.section_footer).to eq(false)
+
+ new_state.new_line!(style: {})
+ new_state.close_section('hello', 101)
+
+ expect(new_state.current_line.section_header).to eq(false)
+ expect(new_state.current_line.section_duration).to eq('00:01')
+ expect(new_state.current_line.section_footer).to eq(true)
+ end
+
it 'ignores bad input', :aggregate_failures do
expect(::Gitlab::AppLogger).to(
receive(:warn).with(
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
index 98fca40e8ea..23be3209171 100644
--- a/spec/lib/gitlab/ci/ansi2json_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -145,6 +145,7 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 63,
content: [],
section_duration: '01:03',
+ section_footer: true,
section: 'prepare-script'
}
])
@@ -163,7 +164,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 56,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -181,7 +183,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 49,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 91,
@@ -262,7 +265,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 75,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -300,7 +304,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 106,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 155,
@@ -311,7 +316,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 158,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
},
{
offset: 200,
@@ -345,13 +351,15 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 115,
content: [],
section: 'prepare-script-nested',
- section_duration: '00:02'
+ section_duration: '00:02',
+ section_footer: true
},
{
offset: 164,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -378,7 +386,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 83,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
])
end
@@ -554,7 +563,8 @@ RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration
offset: 77,
content: [],
section: 'prepare-script',
- section_duration: '01:03'
+ section_duration: '01:03',
+ section_footer: true
}
]
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 3feb09bea02..6b1c0fb2e81 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
- using RSpec::Parameterized::TableSyntax
include RedisHelpers
let_it_be(:redis_store_class) { define_helper_redis_store_class }
@@ -81,113 +80,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
multi_store.send(name, *args, **kwargs)
end
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1" }
- let_it_be(:value2) { "redis_value2" }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:skey2) { "redis:set:key2" }
- let_it_be(:smemberargs) { [skey, value1] }
- let_it_be(:hkey) { "redis:hash:key" }
- let_it_be(:hkey2) { "redis:hash:key2" }
- let_it_be(:zkey) { "redis:sortedset:key" }
- let_it_be(:zkey2) { "redis:sortedset:key2" }
- let_it_be(:hitem1) { "item1" }
- let_it_be(:hitem2) { "item2" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
- let_it_be(:hgetargs) { [hkey, hitem1] }
- let_it_be(:hmgetval) { [value1] }
- let_it_be(:mhmgetargs) { [hkey, hitem1] }
- let_it_be(:hvalmapped) { { "item1" => value1 } }
- let_it_be(:sscanargs) { [skey2, 0] }
- let_it_be(:sscanval) { ["0", [value1]] }
- let_it_be(:scanargs) { ["0"] }
- let_it_be(:scankwargs) { { match: '*:set:key2*' } }
- let_it_be(:scanval) { ["0", [skey2]] }
- let_it_be(:sscan_eachval) { [value1] }
- let_it_be(:sscan_each_arg) { { match: '*1*' } }
- let_it_be(:hscan_eachval) { [[hitem1, value1]] }
- let_it_be(:zscan_eachval) { [[value1, 1.0]] }
- let_it_be(:scan_each_arg) { { match: 'redis*' } }
- let_it_be(:scan_each_val) { [key1, key2, skey, skey2, hkey, hkey2, zkey, zkey2] }
-
- # rubocop:disable Layout/LineLength
- where(:case_name, :name, :args, :value, :kwargs, :block) do
- 'execute :get command' | :get | ref(:key1) | ref(:value1) | {} | nil
- 'execute :mget command' | :mget | ref(:keys) | ref(:values) | {} | nil
- 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | {} | ->(value) { value }
- 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | {} | nil
- 'execute :scard command' | :scard | ref(:skey) | 2 | {} | nil
- 'execute :sismember command' | :sismember | ref(:smemberargs) | true | {} | nil
- 'execute :exists command' | :exists | ref(:key1) | 1 | {} | nil
- 'execute :exists? command' | :exists? | ref(:key1) | true | {} | nil
- 'execute :hget command' | :hget | ref(:hgetargs) | ref(:value1) | {} | nil
- 'execute :hlen command' | :hlen | ref(:hkey) | 1 | {} | nil
- 'execute :hgetall command' | :hgetall | ref(:hkey) | ref(:hvalmapped) | {} | nil
- 'execute :hexists command' | :hexists | ref(:hgetargs) | true | {} | nil
- 'execute :hmget command' | :hmget | ref(:hgetargs) | ref(:hmgetval) | {} | nil
- 'execute :mapped_hmget command' | :mapped_hmget | ref(:mhmgetargs) | ref(:hvalmapped) | {} | nil
- 'execute :sscan command' | :sscan | ref(:sscanargs) | ref(:sscanval) | {} | nil
- 'execute :scan command' | :scan | ref(:scanargs) | ref(:scanval) | ref(:scankwargs) | nil
-
- # we run *scan_each here as they are reads too
- 'execute :scan_each command' | :scan_each | nil | ref(:scan_each_val) | ref(:scan_each_arg) | nil
- 'execute :sscan_each command' | :sscan_each | ref(:skey2) | ref(:sscan_eachval) | {} | nil
- 'execute :sscan_each w block' | :sscan_each | ref(:skey) | ref(:sscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :hscan_each command' | :hscan_each | ref(:hkey) | ref(:hscan_eachval) | {} | nil
- 'execute :hscan_each w block' | :hscan_each | ref(:hkey2) | ref(:hscan_eachval) | ref(:sscan_each_arg) | nil
- 'execute :zscan_each command' | :zscan_each | ref(:zkey) | ref(:zscan_eachval) | {} | nil
- 'execute :zscan_each w block' | :zscan_each | ref(:zkey2) | ref(:zscan_eachval) | ref(:sscan_each_arg) | nil
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.set(key1, value1)
- primary_store.set(key2, value2)
- primary_store.sadd?(skey, [value1, value2])
- primary_store.sadd?(skey2, [value1])
- primary_store.hset(hkey, hitem1, value1)
- primary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- primary_store.zadd(zkey, 1, value1)
- primary_store.zadd(zkey2, [[1, value1], [2, value2]])
-
- secondary_store.set(key1, value1)
- secondary_store.set(key2, value2)
- secondary_store.sadd?(skey, [value1, value2])
- secondary_store.sadd?(skey2, [value1])
- secondary_store.hset(hkey, hitem1, value1)
- secondary_store.hset(hkey2, hitem1, value1, hitem2, value2)
- secondary_store.zadd(zkey, 1, value1)
- secondary_store.zadd(zkey2, [[1, value1], [2, value2]])
- end
-
- after do
- primary_store.flushdb
- secondary_store.flushdb
- end
-
- RSpec.shared_examples_for 'reads correct value' do
- it 'returns the correct value' do
- if value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values (unsorted set)
- is_expected.to match_array(value)
- else
- is_expected.to eq(value)
- end
- end
- end
+ let(:args) { 'args' }
+ let(:kwargs) { { match: '*:set:key2*' } }
RSpec.shared_examples_for 'secondary store' do
it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
subject
end
- include_examples 'reads correct value'
-
it 'does not execute on the primary store' do
expect(primary_store).not_to receive(name)
@@ -195,23 +97,22 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- with_them do
+ described_class::READ_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { kwargs&.present? ? [*args, { **kwargs }] : Array(args) }
+ let(:expected_args) { [*args, { **kwargs }] }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when reading from the primary is successful' do
it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
subject
end
-
- include_examples 'reads correct value'
end
context 'when reading from default instance is raising an exception' do
@@ -229,17 +130,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- context 'when reading from empty default instance' do
- before do
- # this ensures a cache miss without having to stub the default store
- multi_store.default_store.flushdb
- end
-
- it 'does not call the non_default_store' do
- expect(multi_store.non_default_store).not_to receive(name)
- end
- end
-
context 'when the command is executed within pipelined block' do
subject do
multi_store.pipelined do |pipeline|
@@ -253,7 +143,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
2.times do
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*expected_args).once
end
end
@@ -261,27 +151,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- if params[:block]
+ context 'when block provided' do
subject do
- multi_store.send(name, *expected_args, &block)
+ multi_store.send(name, expected_args) { nil }
end
- context 'when block is provided' do
- it 'only default store yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
- expect(secondary_store).not_to receive(name).and_yield(value)
+ it 'only default store to execute' do
+ expect(primary_store).to receive(:send).with(name, expected_args)
+ expect(secondary_store).not_to receive(:send)
- subject
- end
-
- it 'only default store to execute' do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
+ subject
end
end
@@ -304,8 +183,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args)
+ expect(primary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -313,8 +192,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*expected_args)
+ expect(secondary_store).not_to receive(name).with(*expected_args)
subject
end
@@ -406,110 +285,24 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_failures do
- subject
-
- redis_store = multi_store.send(store)
-
- if expected_value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values
- expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
- else
- expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
- end
- end
- end
-
- # rubocop:disable RSpec/MultipleMemoizedHelpers
context 'with WRITE redis commands' do
- let_it_be(:ikey1) { "counter1" }
- let_it_be(:ikey2) { "counter2" }
- let_it_be(:iargs) { [ikey2, 3] }
- let_it_be(:ivalue1) { "1" }
- let_it_be(:ivalue2) { "3" }
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:key3) { "redis:{1}:key_c" }
- let_it_be(:key4) { "redis:{1}:key_d" }
- let_it_be(:value1) { "redis_value1" }
- let_it_be(:value2) { "redis_value2" }
- let_it_be(:key1_value1) { [key1, value1] }
- let_it_be(:key1_value2) { [key1, value2] }
- let_it_be(:ttl) { 10 }
- let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:svalues1) { [value2, value1] }
- let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, [value1]] }
- let_it_be(:skey_value2) { [skey, [value2]] }
- let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) }
- let_it_be(:hkey1) { "redis:{1}:hash_a" }
- let_it_be(:hkey2) { "redis:{1}:hash_b" }
- let_it_be(:item) { "item" }
- let_it_be(:hdelarg) { [hkey1, item] }
- let_it_be(:hsetarg) { [hkey2, item, value1] }
- let_it_be(:mhsetarg) { [hkey2, { "item" => value1 }] }
- let_it_be(:hgetarg) { [hkey2, item] }
- let_it_be(:expireargs) { [key3, ttl] }
-
- # rubocop:disable Layout/LineLength
- where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
- 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
- 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
- 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
- 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :sadd? command' | :sadd? | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
- 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
- 'execute :unlink command' | :unlink | ref(:key3) | nil | :get | ref(:key3)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1)
- 'execute :incr command' | :incr | ref(:ikey1) | ref(:ivalue1) | :get | ref(:ikey1)
- 'execute :incrby command' | :incrby | ref(:iargs) | ref(:ivalue2) | :get | ref(:ikey2)
- 'execute :hset command' | :hset | ref(:hsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- 'execute :hdel command' | :hdel | ref(:hdelarg) | nil | :hget | ref(:hdelarg)
- 'execute :expire command' | :expire | ref(:expireargs) | ref(:ttl) | :ttl | ref(:key3)
- 'execute :mapped_hmset command' | :mapped_hmset | ref(:mhsetarg) | ref(:value1) | :hget | ref(:hgetarg)
- end
- # rubocop:enable Layout/LineLength
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.set(key2, value1)
- primary_store.set(key3, value1)
- primary_store.set(key4, value1)
- primary_store.sadd?(skey, value1)
- primary_store.hset(hkey2, item, value1)
-
- secondary_store.set(key2, value1)
- secondary_store.set(key3, value1)
- secondary_store.set(key4, value1)
- secondary_store.sadd?(skey, value1)
- secondary_store.hset(hkey2, item, value1)
- end
-
- with_them do
+ described_class::WRITE_COMMANDS.each do |name|
describe name.to_s do
- let(:expected_args) { args || no_args }
+ let(:args) { "dummy_args" }
+ let(:name) { name }
before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
+ allow(primary_store).to receive(name)
+ allow(secondary_store).to receive(name)
end
context 'when executing on primary instance is successful' do
it 'executes on both primary and secondary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
context 'when use_primary_and_secondary_stores feature flag is disabled' do
@@ -523,8 +316,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'executes only on secondary redis store', :aggregate_failures do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*args)
+ expect(primary_store).not_to receive(name).with(*args)
subject
end
@@ -532,8 +325,8 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when using primary store as default' do
it 'executes only on primary redis store', :aggregate_failures do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
+ expect(primary_store).to receive(name).with(*args)
+ expect(secondary_store).not_to receive(name).with(*args)
subject
end
@@ -542,19 +335,19 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when executing on the default instance is raising an exception' do
before do
- allow(multi_store.default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
it 'raises error and does not execute on non default instance', :aggregate_failures do
- expect(multi_store.non_default_store).not_to receive(name).with(*expected_args)
+ expect(multi_store.non_default_store).not_to receive(name).with(*args)
expect { subject }.to raise_error(StandardError)
end
end
context 'when executing on the non default instance is raising an exception' do
before do
- allow(multi_store.non_default_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.non_default_store).to receive(name).with(*args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
@@ -562,12 +355,10 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(:multi_store_error_message,
command_name: name, instance_name: instance_name))
- expect(multi_store.default_store).to receive(name).with(*expected_args).and_call_original
+ expect(multi_store.default_store).to receive(name).with(*args)
subject
end
-
- include_examples 'verify that store contains values', :default_store
end
context 'when the command is executed within pipelined block' do
@@ -580,103 +371,32 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'is executed only 1 time on each instance', :aggregate_failures do
expect(primary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
expect(secondary_store).to receive(:pipelined).and_call_original
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
+ expect(pipeline).to receive(name).with(*args).once
end
subject
end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
end
end
end
end
- # rubocop:enable RSpec/MultipleMemoizedHelpers
-
- context 'with mocked redis commands' do
- let(:args) { [1, 2, 3] }
- let(:kwargs) { { foo: 'bar' } }
-
- subject do
- multi_store.send(command, *args, **kwargs)
- end
-
- context 'for read commands' do
- described_class::READ_COMMANDS.each do |command|
- describe command.to_s do
- let(:command) { command }
-
- where(
- :use_primary_and_secondary_stores_ff,
- :use_primary_store_as_default_ff,
- :executed_store,
- :non_executed_store,
- :executed_store_name
- ) do
- false | false | ref(:secondary_store) | ref(:primary_store) | 'secondary_store'
- true | false | ref(:secondary_store) | ref(:primary_store) | 'secondary_store'
- true | true | ref(:primary_store) | ref(:secondary_store) | 'primary_store'
- false | true | ref(:primary_store) | ref(:secondary_store) | 'primary_store'
- end
-
- with_them do
- before do
- stub_feature_flags(
- use_primary_and_secondary_stores_for_test_store: use_primary_and_secondary_stores_ff,
- use_primary_store_as_default_for_test_store: use_primary_store_as_default_ff
- )
- end
-
- it "executes on #{params[:executed_store_name]}" do
- expect(executed_store).to receive(command).with(*args, **kwargs)
- expect(non_executed_store).not_to receive(command)
- subject
- end
- end
- end
- end
- end
-
- context 'for write commands' do
- described_class::WRITE_COMMANDS.each do |command|
- describe command.to_s do
- let(:command) { command }
-
- where(
- :use_primary_and_secondary_stores_ff,
- :use_primary_store_as_default_ff,
- :executed_stores,
- :non_executed_store
- ) do
- false | false | [ref(:secondary_store)] | ref(:primary_store)
- true | false | [ref(:secondary_store), ref(:primary_store)] | []
- true | true | [ref(:primary_store), ref(:secondary_store)] | []
- false | true | [ref(:primary_store)] | ref(:secondary_store)
- end
-
- with_them do
- before do
- stub_feature_flags(
- use_primary_and_secondary_stores_for_test_store: use_primary_and_secondary_stores_ff,
- use_primary_store_as_default_for_test_store: use_primary_store_as_default_ff
- )
- end
+ RSpec.shared_examples_for 'verify that store contains values' do |store|
+ it "#{store} redis store contains correct values", :aggregate_failures do
+ subject
- it "executes on executed_stores" do
- expect(executed_stores).to all(receive(command).with(*args, **kwargs).ordered)
- expect(non_executed_store).not_to receive(command)
+ redis_store = multi_store.send(store)
- subject
- end
- end
- end
+ if expected_value.is_a?(Array)
+ # :smembers does not guarantee the order it will return the values
+ expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
+ else
+ expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
end
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index ab92936440c..ad568e60d5c 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do
allow(project).to receive(:branch_allows_collaboration?).and_return(true)
end
- it 'enables update_build if user is maintainer' do
+ it 'enables updates if user is maintainer', :aggregate_failures do
expect(policy).to be_allowed :cancel_build
expect(policy).to be_allowed :update_build
expect(policy).to be_allowed :update_commit_status
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 2ab112a8527..382aabd45a1 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -791,14 +791,14 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
context 'authorized user' do
- context 'user with :update_build persmission' do
+ context 'user with :cancel_build permission' do
it 'cancels running or pending job' do
expect(response).to have_gitlab_http_status(:created)
expect(project.builds.first.status).to eq('success')
end
end
- context 'user without :update_build permission' do
+ context 'user without :cancel_build permission' do
let(:api_user) { reporter }
it 'does not cancel job' do
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 166768ea605..a1d29c4a935 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -461,6 +461,18 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category:
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ it "rotates user's own token", :freeze_time do
+ expiry_date = Date.today + 1.month
+
+ post(api(path, token.user), params: { expires_at: expiry_date })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index dcb6572d413..01e02651a64 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -477,6 +477,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:token) { create(:personal_access_token, user: project_bot) }
let_it_be(:resource_id) { resource.id }
let_it_be(:token_id) { token.id }
+ let(:params) { {} }
let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" }
@@ -485,7 +486,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
resource.add_owner(user)
end
- subject(:rotate_token) { post api(path, user) }
+ subject(:rotate_token) { post(api(path, user), params: params) }
it "allows owner to rotate token", :freeze_time do
rotate_token
@@ -495,6 +496,19 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
end
+ context 'when expiry is defined' do
+ let(:expiry_date) { Date.today + 1.month }
+ let(:params) { { expires_at: expiry_date } }
+
+ it "allows owner to rotate token", :freeze_time do
+ rotate_token
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq(expiry_date.to_s)
+ end
+ end
+
context 'without permission' do
it 'returns an error message' do
another_user = create(:user)
diff --git a/spec/serializers/ci/job_entity_spec.rb b/spec/serializers/ci/job_entity_spec.rb
index 6dce87a1fc5..c3d0de11405 100644
--- a/spec/serializers/ci/job_entity_spec.rb
+++ b/spec/serializers/ci/job_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobEntity do
+RSpec.describe Ci::JobEntity, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:job) { create(:ci_build, :running) }
let(:project) { job.project }
diff --git a/spec/services/ci/catalog/resources/release_service_spec.rb b/spec/services/ci/catalog/resources/release_service_spec.rb
new file mode 100644
index 00000000000..1901485d402
--- /dev/null
+++ b/spec/services/ci/catalog/resources/release_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ context 'with a valid catalog resource and release' do
+ it 'validates the catalog resource and creates a version' do
+ project = create(:project, :catalog_resource_with_components)
+ catalog_resource = create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ version = Ci::Catalog::Resources::Version.last
+
+ expect(response).to be_success
+ expect(version.release).to eq(release)
+ expect(version.catalog_resource).to eq(catalog_resource)
+ expect(version.catalog_resource.project).to eq(project)
+ end
+ end
+
+ context 'when the validation of the catalog resource fails' do
+ it 'returns an error and does not create a version' do
+ project = create(:project, :repository)
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to eq('Project must have a description, Project must contain components')
+ end
+ end
+
+ context 'when the creation of a version fails' do
+ it 'returns an error and does not create a version' do
+ project =
+ create(
+ :project, :custom_repo,
+ description: 'Component project',
+ files: {
+ 'templates/secret-detection.yml' => 'image: agent: coop',
+ 'README.md' => 'Read me'
+ }
+ )
+ create(:ci_catalog_resource, project: project)
+ release = create(:release, project: project, sha: project.repository.root_ref_sha)
+
+ response = described_class.new(release).execute
+
+ expect(Ci::Catalog::Resources::Version.count).to be(0)
+ expect(response).to be_error
+ expect(response.message).to include('mapping values are not allowed in this context')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 80fbfc04f9b..1646afde21d 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -270,14 +270,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_bridge
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_bridge
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
@@ -302,14 +294,6 @@ RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
it_behaves_like 'creates associations for a deployable job', :ci_build
end
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it_behaves_like 'creates associations for a deployable job', :ci_build
- end
-
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index ab578f19d5f..b28d7549fbb 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -56,16 +56,17 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
end
context 'when project is a catalog resource' do
- let(:ref) { 'master' }
+ let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') }
let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) }
+ let(:ref) { 'master' }
context 'and it is valid' do
- let_it_be(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') }
-
it_behaves_like 'a successful release creation'
end
- context 'and it is invalid' do
+ context 'and it is an invalid resource' do
+ let_it_be(:project) { create(:project, :repository) }
+
it 'raises an error and does not update the release' do
result = service.execute
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 5014a810f35..68eb3539813 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -54,7 +54,7 @@ RSpec.shared_context 'ProjectPolicy context' do
create_environment create_merge_request_from
admin_metrics_dashboard_annotation create_pipeline create_release
create_wiki destroy_container_image push_code read_pod_logs
- read_terraform_state resolve_note update_build update_commit_status
+ read_terraform_state resolve_note update_build cancel_build update_commit_status
update_container_image update_deployment update_environment
update_merge_request update_pipeline update_release destroy_release
read_resource_group update_resource_group update_escalation_status
diff --git a/spec/workers/ci/initial_pipeline_process_worker_spec.rb b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
index 9a94f1cbb4c..fcdd0a2a33b 100644
--- a/spec/workers/ci/initial_pipeline_process_worker_spec.rb
+++ b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
@@ -70,32 +70,6 @@ RSpec.describe Ci::InitialPipelineProcessWorker, feature_category: :continuous_i
subject
end
-
- context 'when `create_deployment_only_for_processable_jobs` FF is disabled' do
- before do
- stub_feature_flags(create_deployment_only_for_processable_jobs: false)
- end
-
- it 'creates a deployment record' do
- expect { subject }.to change { Deployment.count }.by(1)
-
- expect(job.deployment).to have_attributes(
- project: job.project,
- ref: job.ref,
- sha: job.sha,
- deployable: job,
- deployable_type: 'CommitStatus',
- environment: job.persisted_environment
- )
- end
-
- it 'a deployment is created before atomic processing is kicked off' do
- expect(::Deployments::CreateForJobService).to receive(:new).ordered
- expect(::Ci::PipelineProcessing::AtomicProcessingService).to receive(:new).ordered
-
- subject
- end
- end
end
end
end