diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-27 15:09:34 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-27 15:09:34 +0000 |
commit | 4c39dd11dcbdab4fdd9424a62320a1fc773c2918 (patch) | |
tree | b3bc6139fdc8d1e3254304222f06f82821c5aa64 | |
parent | 95ff19a65c5236863e4c7c7e198bfc1e2fa70f07 (diff) | |
download | gitlab-ce-4c39dd11dcbdab4fdd9424a62320a1fc773c2918.tar.gz |
Add latest changes from gitlab-org/gitlab@master
27 files changed, 841 insertions, 229 deletions
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js index 5487aeb9391..813e21b6ce9 100644 --- a/app/assets/javascripts/graphql_shared/utils.js +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -14,3 +14,41 @@ export const MutationOperationMode = { Remove: 'REMOVE', Replace: 'REPLACE', }; + +/** + * Possible GraphQL entity types. + */ +export const TYPE_GROUP = 'Group'; + +/** + * Ids generated by GraphQL endpoints are usually in the format + * gid://gitlab/Groups/123. This method takes a type and an id + * and interpolates the 2 values into the expected GraphQL ID format. + * + * @param {String} type The entity type + * @param {String|Number} id The id value + * @returns {String} + */ +export const convertToGraphQLId = (type, id) => { + if (typeof type !== 'string') { + throw new TypeError(`type must be a string; got ${typeof type}`); + } + + if (!['number', 'string'].includes(typeof id)) { + throw new TypeError(`id must be a number or string; got ${typeof id}`); + } + + return `gid://gitlab/${type}/${id}`; +}; + +/** + * Ids generated by GraphQL endpoints are usually in the format + * gid://gitlab/Groups/123. This method takes a type and an + * array of ids and tranforms the array values into the expected + * GraphQL ID format. + * + * @param {String} type The entity type + * @param {Array} ids An array of id values + * @returns {Array} + */ +export const convertToGraphQLIds = (type, ids) => ids.map(id => convertToGraphQLId(type, id)); diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue index af3220840a6..c9f1c8b903c 100644 --- a/app/assets/javascripts/packages/details/components/app.vue +++ b/app/assets/javascripts/packages/details/components/app.vue @@ -5,29 +5,26 @@ import { GlModal, GlModalDirective, GlTooltipDirective, - GlLink, GlEmptyState, GlTab, GlTabs, - GlTable, GlSprintf, } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; import Tracking from '~/tracking'; +import { s__ } from '~/locale'; +import { objectToQueryString } from '~/lib/utils/common_utils'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; import PackageHistory from './package_history.vue'; import PackageTitle from './package_title.vue'; import PackagesListLoader from '../../shared/components/packages_list_loader.vue'; import PackageListRow from '../../shared/components/package_list_row.vue'; +import { packageTypeToTrackCategory } from '../../shared/utils'; +import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants'; import DependencyRow from './dependency_row.vue'; import AdditionalMetadata from './additional_metadata.vue'; import InstallationCommands from './installation_commands.vue'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; -import FileIcon from '~/vue_shared/components/file_icon.vue'; -import { __, s__ } from '~/locale'; -import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants'; -import { packageTypeToTrackCategory } from '../../shared/utils'; -import { objectToQueryString } from '~/lib/utils/common_utils'; +import PackageFiles from './package_files.vue'; export default { name: 'PackagesApp', @@ -35,12 +32,9 @@ export default { GlBadge, GlButton, GlEmptyState, - GlLink, GlModal, GlTab, GlTabs, - GlTable, - FileIcon, GlSprintf, PackageTitle, PackagesListLoader, @@ -49,12 +43,13 @@ export default { PackageHistory, AdditionalMetadata, InstallationCommands, + PackageFiles, }, directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective, }, - mixins: [timeagoMixin, Tracking.mixin()], + mixins: [Tracking.mixin()], trackingActions: { ...TrackingActions }, computed: { ...mapState([ @@ -72,14 +67,6 @@ export default { isValidPackage() { return Boolean(this.packageEntity.name); }, - filesTableRows() { - return this.packageFiles.map(x => ({ - name: x.file_name, - downloadPath: x.download_path, - size: this.formatSize(x.size), - created: x.created_at, - })); - }, tracking() { return { category: packageTypeToTrackCategory(this.packageEntity.package_type), @@ -128,22 +115,6 @@ export default { `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`, ), }, - filesTableHeaderFields: [ - { - key: 'name', - label: __('Name'), - tdClass: 'd-flex align-items-center', - }, - { - key: 'size', - label: __('Size'), - }, - { - key: 'created', - label: __('Created'), - class: 'text-right', - }, - ], }; </script> @@ -185,35 +156,11 @@ export default { <additional-metadata :package-entity="packageEntity" /> </div> - <template v-if="showFiles"> - <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> - <gl-table - :fields="$options.filesTableHeaderFields" - :items="filesTableRows" - tbody-tr-class="js-file-row" - > - <template #cell(name)="{ item }"> - <gl-link - :href="item.downloadPath" - class="js-file-download gl-relative" - @click="track($options.trackingActions.PULL_PACKAGE)" - > - <file-icon - :file-name="item.name" - css-classes="gl-relative file-icon" - class="gl-mr-1 gl-relative" - /> - <span class="gl-relative">{{ item.name }}</span> - </gl-link> - </template> - - <template #cell(created)="{ item }"> - <span v-gl-tooltip :title="tooltipTitle(item.created)">{{ - timeFormatted(item.created) - }}</span> - </template> - </gl-table> - </template> + <package-files + v-if="showFiles" + :package-files="packageFiles" + @download-file="track($options.trackingActions.PULL_PACKAGE)" + /> </gl-tab> <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab"> diff --git a/app/assets/javascripts/packages/details/components/package_files.vue b/app/assets/javascripts/packages/details/components/package_files.vue new file mode 100644 index 00000000000..62eaff6c3c7 --- /dev/null +++ b/app/assets/javascripts/packages/details/components/package_files.vue @@ -0,0 +1,86 @@ +<script> +import { GlLink, GlTable } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Tracking from '~/tracking'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; + +export default { + name: 'PackageFiles', + components: { + GlLink, + GlTable, + FileIcon, + TimeAgoTooltip, + }, + mixins: [Tracking.mixin()], + props: { + packageFiles: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + filesTableRows() { + return this.packageFiles.map(pf => ({ + ...pf, + size: this.formatSize(pf.size), + })); + }, + }, + methods: { + formatSize(size) { + return numberToHumanSize(size); + }, + }, + filesTableHeaderFields: [ + { + key: 'name', + label: __('Name'), + tdClass: 'gl-display-flex gl-align-items-center', + }, + { + key: 'size', + label: __('Size'), + }, + { + key: 'created', + label: __('Created'), + class: 'gl-text-right', + }, + ], +}; +</script> + +<template> + <div> + <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> + <gl-table + :fields="$options.filesTableHeaderFields" + :items="filesTableRows" + :tbody-tr-attr="{ 'data-testid': 'file-row' }" + > + <template #cell(name)="{ item }"> + <gl-link + :href="item.download_path" + class="gl-relative" + data-testid="download-link" + @click="$emit('download-file')" + > + <file-icon + :file-name="item.file_name" + css-classes="gl-relative file-icon" + class="gl-mr-1 gl-relative" + /> + <span class="gl-relative">{{ item.file_name }}</span> + </gl-link> + </template> + + <template #cell(created)="{ item }"> + <time-ago-tooltip :time="item.created_at" /> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue new file mode 100644 index 00000000000..49a2feab0fc --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -0,0 +1,89 @@ +<script> +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { DEFAULT, LOAD_FAILURE } from '../../constants'; +import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql'; +import PipelineGraph from './graph_component.vue'; +import { unwrapPipelineData } from './utils'; + +export default { + name: 'PipelineGraphWrapper', + components: { + GlAlert, + GlLoadingIcon, + PipelineGraph, + }, + inject: { + pipelineIid: { + default: '', + }, + pipelineProjectPath: { + default: '', + }, + }, + data() { + return { + pipeline: null, + alertType: null, + showAlert: false, + }; + }, + errorTexts: { + [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'), + [DEFAULT]: __('An unknown error occurred while loading this graph.'), + }, + apollo: { + pipeline: { + query: getPipelineDetails, + variables() { + return { + projectPath: this.pipelineProjectPath, + iid: this.pipelineIid, + }; + }, + update(data) { + return unwrapPipelineData(this.pipelineIid, data); + }, + error() { + this.reportFailure(LOAD_FAILURE); + }, + }, + }, + computed: { + alert() { + switch (this.alertType) { + case LOAD_FAILURE: + return { + text: this.$options.errorTexts[LOAD_FAILURE], + variant: 'danger', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } + }, + }, + methods: { + hideAlert() { + this.showAlert = false; + }, + reportFailure(type) { + this.showAlert = true; + this.failureType = type; + }, + }, +}; +</script> +<template> + <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert"> + {{ alert.text }} + </gl-alert> + <gl-loading-icon + v-else-if="$apollo.queries.pipeline.loading" + class="gl-mx-auto gl-my-4" + size="lg" + /> + <pipeline-graph v-else :pipeline="pipeline" /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue index c5f30c8aef0..78b69073cd3 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -29,11 +29,13 @@ export default { </div> <div class="col-12"> - <div class="text-content"> + <div class="gl-text-content"> <template v-if="canSetCi"> - <h4 class="text-center">{{ s__('Pipelines|Build with confidence') }}</h4> + <h4 class="gl-text-center" data-testid="header-text"> + {{ s__('Pipelines|Build with confidence') }} + </h4> - <p> + <p data-testid="info-text"> {{ s__(`Pipelines|Continuous Integration can help catch bugs by running your tests automatically, @@ -42,12 +44,11 @@ export default { }} </p> - <div class="text-center"> + <div class="gl-text-center"> <gl-button :href="helpPagePath" variant="info" category="primary" - class="js-get-started-pipelines" data-testid="get-started-pipelines" > {{ s__('Pipelines|Get started with Pipelines') }} @@ -55,7 +56,7 @@ export default { </div> </template> - <p v-else class="text-center"> + <p v-else class="gl-text-center"> {{ s__('Pipelines|This project is not currently set up to run pipelines.') }} </p> </div> diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql new file mode 100644 index 00000000000..6d80e7b8d51 --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql @@ -0,0 +1,54 @@ +query getPipelineDetails($projectPath: ID!, $iid: ID!) { + project(fullPath: $projectPath) { + pipeline(iid: $iid) { + id: iid + stages { + nodes { + name + status: detailedStatus { + action { + icon + path + title + } + } + groups { + nodes { + status: detailedStatus { + label + group + icon + } + name + size + jobs { + nodes { + name + scheduledAt + needs { + nodes { + name + } + } + status: detailedStatus { + icon + tooltip + hasDetails + detailsPath + group + action { + buttonTitle + icon + path + title + } + } + } + } + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 313bb6dfa9d..27f71d2b878 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -149,7 +149,9 @@ export default async function() { const { createPipelinesDetailApp } = await import( /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' ); - createPipelinesDetailApp(); + + const { pipelineProjectPath, pipelineIid } = dataset; + createPipelinesDetailApp(SELECTORS.PIPELINE_DETAILS, pipelineProjectPath, pipelineIid); } catch { Flash(__('An error occurred while loading the pipeline.')); } diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js index 880855cf21d..51e40b95515 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_graph.js +++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js @@ -1,7 +1,30 @@ -const createPipelinesDetailApp = () => { - // Placeholder. See: https://gitlab.com/gitlab-org/gitlab/-/issues/223262 - // eslint-disable-next-line no-useless-return - return; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid) => { + // eslint-disable-next-line no-new + new Vue({ + el: selector, + components: { + PipelineGraphWrapper, + }, + apolloProvider, + provide: { + pipelineProjectPath, + pipelineIid, + }, + render(createElement) { + return createElement(PipelineGraphWrapper); + }, + }); }; export { createPipelinesDetailApp }; diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js index 2c89d508c31..a48da51caae 100644 --- a/app/assets/javascripts/registry/explorer/utils.js +++ b/app/assets/javascripts/registry/explorer/utils.js @@ -1,3 +1,5 @@ +import { joinPaths } from '~/lib/utils/url_utility'; + export const pathGenerator = (imageDetails, ending = '?format=json') => { // this method is a temporary workaround, to be removed with graphql implementation // https://gitlab.com/gitlab-org/gitlab/-/issues/276432 @@ -12,5 +14,12 @@ export const pathGenerator = (imageDetails, ending = '?format=json') => { return acc; }, []) .join('/'); - return `/${basePath}/registry/repository/${imageDetails.id}/tags${ending}`; + + return joinPaths( + window.gon.relative_url_root, + `/${basePath}`, + '/registry/repository/', + `${imageDetails.id}`, + `tags${ending}`, + ); }; diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4bfb38cbe2d..99bc894b812 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -774,6 +774,15 @@ module Ci variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s) variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s) variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s) + + if Feature.enabled?(:ci_mr_diff_variables, project) + diff = self.merge_request_diff + if diff.present? + variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s) + variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha) + end + end + variables.concat(merge_request.predefined_variables) end @@ -845,9 +854,15 @@ module Ci end def same_family_pipeline_ids - ::Gitlab::Ci::PipelineObjectHierarchy.new( - base_and_ancestors(same_project: true), options: { same_project: true } - ).base_and_descendants.select(:id) + if Feature.enabled?(:ci_root_ancestor_for_pipeline_family, project, default_enabled: false) + ::Gitlab::Ci::PipelineObjectHierarchy.new( + self.class.where(id: root_ancestor), options: { same_project: true } + ).base_and_descendants.select(:id) + else + ::Gitlab::Ci::PipelineObjectHierarchy.new( + base_and_ancestors(same_project: true), options: { same_project: true } + ).base_and_descendants.select(:id) + end end def build_with_artifacts_in_self_and_descendants(name) @@ -869,6 +884,15 @@ module Ci .base_and_descendants end + def root_ancestor + return self unless child? + + Gitlab::Ci::PipelineObjectHierarchy + .new(self.class.unscoped.where(id: id), options: { same_project: true }) + .base_and_ancestors(hierarchy_order: :desc) + .first + end + def bridge_triggered? source_bridge.present? end @@ -878,7 +902,8 @@ module Ci end def child? - parent_pipeline.present? + parent_pipeline? && # child pipelines have `parent_pipeline` source + parent_pipeline.present? end def parent? @@ -1139,6 +1164,22 @@ module Ci Gitlab::DataBuilder::Pipeline.build(self) end + def merge_request_diff_sha + return unless merge_request? + + if merge_request_pipeline? + source_sha + else + sha + end + end + + def merge_request_diff + return unless merge_request? + + merge_request.merge_request_diff_for(merge_request_diff_sha) + end + def push_details strong_memoize(:push_details) do Gitlab::Git::Push.new(project, before_sha, sha, git_ref) diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 03f4caccccd..1e41b6f4f31 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.22.0' + VERSION = '0.23.0' self.table_name = 'clusters_applications_runners' diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 0b07fe9921e..847b96cbd0e 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -23,4 +23,4 @@ = render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors -.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } +.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid } } diff --git a/changelogs/unreleased/288752-registry-details-relative-url-fix.yml b/changelogs/unreleased/288752-registry-details-relative-url-fix.yml new file mode 100644 index 00000000000..2d315d396ea --- /dev/null +++ b/changelogs/unreleased/288752-registry-details-relative-url-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix container_registry url for relative urls +merge_request: 48661 +author: +type: fixed diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-23-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-23-0.yml new file mode 100644 index 00000000000..c2890e96944 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-23-0.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Runner Helm Chart to 0.23.0 +merge_request: 48284 +author: +type: other diff --git a/config/feature_flags/development/ci_mr_diff_variables.yml b/config/feature_flags/development/ci_mr_diff_variables.yml new file mode 100644 index 00000000000..2b628a6dc4f --- /dev/null +++ b/config/feature_flags/development/ci_mr_diff_variables.yml @@ -0,0 +1,8 @@ +--- +name: ci_mr_diff_variables +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46621 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285468 +milestone: '13.7' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/ci_root_ancestor_for_pipeline_family.yml b/config/feature_flags/development/ci_root_ancestor_for_pipeline_family.yml new file mode 100644 index 00000000000..5ddc9d92a1c --- /dev/null +++ b/config/feature_flags/development/ci_root_ancestor_for_pipeline_family.yml @@ -0,0 +1,8 @@ +--- +name: ci_root_ancestor_for_pipeline_family +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46575 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/287812 +milestone: '13.7' +type: development +group: group::continuous integration +default_enabled: false diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dda77bec996..90737b6fddb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -30349,6 +30349,9 @@ msgstr "" msgid "We are currently unable to fetch data for this graph." msgstr "" +msgid "We are currently unable to fetch data for this pipeline." +msgstr "" + msgid "We could not determine the path to remove the epic" msgstr "" diff --git a/package.json b/package.json index dc835a80eab..4271237ea5b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.175.0", - "@gitlab/ui": "24.1.0", + "@gitlab/ui": "24.2.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-3", "@rails/ujs": "^6.0.3-2", diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index ea22fba37c9..09dcf026aa3 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -16,6 +16,7 @@ FactoryBot.define do transient { head_pipeline_of { nil } } transient { child_of { nil } } + transient { upstream_of { nil } } after(:build) do |pipeline, evaluator| if evaluator.child_of @@ -30,9 +31,12 @@ FactoryBot.define do if evaluator.child_of bridge = create(:ci_bridge, pipeline: evaluator.child_of) - create(:ci_sources_pipeline, - source_job: bridge, - pipeline: pipeline) + create(:ci_sources_pipeline, source_job: bridge, pipeline: pipeline) + end + + if evaluator.upstream_of + bridge = create(:ci_bridge, pipeline: pipeline) + create(:ci_sources_pipeline, source_job: bridge, pipeline: evaluator.upstream_of) end end diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js index 6a630195126..d392b0f0575 100644 --- a/spec/frontend/graphql_shared/utils_spec.js +++ b/spec/frontend/graphql_shared/utils_spec.js @@ -1,4 +1,12 @@ -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { + getIdFromGraphQLId, + convertToGraphQLId, + convertToGraphQLIds, +} from '~/graphql_shared/utils'; + +const mockType = 'Group'; +const mockId = 12; +const mockGid = `gid://gitlab/Group/12`; describe('getIdFromGraphQLId', () => { [ @@ -44,3 +52,32 @@ describe('getIdFromGraphQLId', () => { }); }); }); + +describe('convertToGraphQLId', () => { + it('combines $type and $id into $result', () => { + expect(convertToGraphQLId(mockType, mockId)).toBe(mockGid); + }); + + it.each` + type | id | message + ${mockType} | ${null} | ${'id must be a number or string; got object'} + ${null} | ${mockId} | ${'type must be a string; got object'} + `('throws TypeError with "$message" if a param is missing', ({ type, id, message }) => { + expect(() => convertToGraphQLId(type, id)).toThrow(new TypeError(message)); + }); +}); + +describe('convertToGraphQLIds', () => { + it('combines $type and $id into $result', () => { + expect(convertToGraphQLIds(mockType, [mockId])).toStrictEqual([mockGid]); + }); + + it.each` + type | ids | message + ${mockType} | ${null} | ${"Cannot read property 'map' of null"} + ${mockType} | ${[mockId, null]} | ${'id must be a number or string; got object'} + ${null} | ${[mockId]} | ${'type must be a string; got object'} + `('throws TypeError with "$message" if a param is missing', ({ type, ids, message }) => { + expect(() => convertToGraphQLIds(type, ids)).toThrow(new TypeError(message)); + }); +}); diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js index e82c74e56e5..97df117df0b 100644 --- a/spec/frontend/packages/details/components/app_spec.js +++ b/spec/frontend/packages/details/components/app_spec.js @@ -16,6 +16,7 @@ import DependencyRow from '~/packages/details/components/dependency_row.vue'; import PackageHistory from '~/packages/details/components/package_history.vue'; import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue'; import InstallationCommands from '~/packages/details/components/installation_commands.vue'; +import PackageFiles from '~/packages/details/components/package_files.vue'; import { composerPackage, @@ -23,7 +24,6 @@ import { mavenPackage, mavenFiles, npmPackage, - npmFiles, nugetPackage, } from '../../mock_data'; @@ -82,8 +82,6 @@ describe('PackagesApp', () => { const packageTitle = () => wrapper.find(PackageTitle); const emptyState = () => wrapper.find(GlEmptyState); - const allFileRows = () => wrapper.findAll('.js-file-row'); - const firstFileDownloadLink = () => wrapper.find('.js-file-download'); const deleteButton = () => wrapper.find('.js-delete-button'); const deleteModal = () => wrapper.find(GlModal); const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' }); @@ -98,6 +96,7 @@ describe('PackagesApp', () => { const findPackageHistory = () => wrapper.find(PackageHistory); const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata); const findInstallationCommands = () => wrapper.find(InstallationCommands); + const findPackageFiles = () => wrapper.find(PackageFiles); beforeEach(() => { delete window.location; @@ -144,28 +143,7 @@ describe('PackagesApp', () => { it('hides the files table if package type is COMPOSER', () => { createComponent({ packageEntity: composerPackage }); - expect(allFileRows().exists()).toBe(false); - }); - - it('renders a single file for an npm package as they only contain one file', () => { - createComponent({ packageEntity: npmPackage, packageFiles: npmFiles }); - - expect(allFileRows()).toExist(); - expect(allFileRows()).toHaveLength(1); - }); - - it('renders multiple files for a package that contains more than one file', () => { - createComponent(); - - expect(allFileRows()).toExist(); - expect(allFileRows()).toHaveLength(2); - }); - - it('allows the user to download a package file by rendering a download link', () => { - createComponent(); - - expect(allFileRows()).toExist(); - expect(firstFileDownloadLink().vm.$attrs.href).toContain('download'); + expect(findPackageFiles().exists()).toBe(false); }); describe('deleting packages', () => { @@ -331,7 +309,7 @@ describe('PackagesApp', () => { it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => { createComponent({ packageEntity: conanPackage }); - firstFileDownloadLink().vm.$emit('click'); + findPackageFiles().vm.$emit('download-file'); expect(eventSpy).toHaveBeenCalledWith( category, TrackingActions.PULL_PACKAGE, diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages/details/components/package_files_spec.js new file mode 100644 index 00000000000..fe1d51cecd4 --- /dev/null +++ b/spec/frontend/packages/details/components/package_files_spec.js @@ -0,0 +1,99 @@ +import { mount } from '@vue/test-utils'; +import stubChildren from 'helpers/stub_children'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import component from '~/packages/details/components/package_files.vue'; + +import { npmFiles, mavenFiles } from '../../mock_data'; + +describe('Package Files', () => { + let wrapper; + + const findAllRows = () => wrapper.findAll('[data-testid="file-row"'); + const findFirstRow = () => findAllRows().at(0); + const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"'); + const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); + const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); + + const createComponent = (packageFiles = npmFiles) => { + wrapper = mount(component, { + propsData: { + packageFiles, + }, + stubs: { + ...stubChildren(component), + GlTable: false, + GlLink: '<div><slot></slot></div>', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('rows', () => { + it('renders a single file for an npm package', () => { + createComponent(); + + expect(findAllRows()).toHaveLength(1); + }); + + it('renders multiple files for a package that contains more than one file', () => { + createComponent(mavenFiles); + + expect(findAllRows()).toHaveLength(2); + }); + }); + + describe('link', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowDownloadLink().exists()).toBe(true); + }); + + it('has the correct attrs bound', () => { + createComponent(); + + expect(findFirstRowDownloadLink().attributes('href')).toBe(npmFiles[0].download_path); + }); + + it('emits "download-file" event on click', () => { + createComponent(); + + findFirstRowDownloadLink().vm.$emit('click'); + + expect(wrapper.emitted('download-file')).toEqual([[]]); + }); + }); + + describe('file-icon', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowFileIcon().exists()).toBe(true); + }); + + it('has the correct props bound', () => { + createComponent(); + + expect(findFirstRowFileIcon().props('fileName')).toBe(npmFiles[0].file_name); + }); + }); + + describe('time-ago tooltip', () => { + it('exists', () => { + createComponent(); + + expect(findFirstRowCreatedAt().exists()).toBe(true); + }); + + it('has the correct props bound', () => { + createComponent(); + + expect(findFirstRowCreatedAt().props('time')).toBe(npmFiles[0].created_at); + }); + }); +}); diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js index 79356664834..28a73c8863c 100644 --- a/spec/frontend/pipelines/empty_state_spec.js +++ b/spec/frontend/pipelines/empty_state_spec.js @@ -1,58 +1,52 @@ -import Vue from 'vue'; -import emptyStateComp from '~/pipelines/components/pipelines_list/empty_state.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; describe('Pipelines Empty State', () => { - let component; - let EmptyStateComponent; + let wrapper; + + const findGetStartedButton = () => wrapper.find('[data-testid="get-started-pipelines"]'); + const findInfoText = () => wrapper.find('[data-testid="info-text"]').text(); + const createWrapper = () => { + wrapper = shallowMount(EmptyState, { + propsData: { + helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + canSetCi: true, + }, + }); + }; - beforeEach(() => { - EmptyStateComponent = Vue.extend(emptyStateComp); + describe('renders', () => { + beforeEach(() => { + createWrapper(); + }); - component = mountComponent(EmptyStateComponent, { - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - canSetCi: true, + afterEach(() => { + wrapper.destroy(); + wrapper = null; }); - }); - afterEach(() => { - component.$destroy(); - }); + it('should render empty state SVG', () => { + expect(wrapper.find('img').attributes('src')).toBe('foo'); + }); - it('should render empty state SVG', () => { - expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); - }); + it('should render empty state header', () => { + expect(wrapper.find('[data-testid="header-text"]').text()).toBe('Build with confidence'); + }); - it('should render empty state information', () => { - expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); - - expect( - component.$el - .querySelector('p') - .innerHTML.trim() - .replace(/\n+\s+/m, ' ') - .replace(/\s\s+/g, ' '), - ).toContain('Continuous Integration can help catch bugs by running your tests automatically,'); - - expect( - component.$el - .querySelector('p') - .innerHTML.trim() - .replace(/\n+\s+/m, ' ') - .replace(/\s\s+/g, ' '), - ).toContain( - 'while Continuous Deployment can help you deliver code to your product environment', - ); - }); + it('should render a link with provided help path', () => { + expect(findGetStartedButton().attributes('href')).toBe('foo'); + }); - it('should render a link with provided help path', () => { - expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual( - 'foo', - ); + it('should render empty state information', () => { + expect(findInfoText()).toContain( + 'Continuous Integration can help catch bugs by running your tests automatically', + 'while Continuous Deployment can help you deliver code to your product environment', + ); + }); - expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain( - 'Get started with Pipelines', - ); + it('should render a button', () => { + expect(findGetStartedButton().text()).toBe('Get started with Pipelines'); + }); }); }); diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js new file mode 100644 index 00000000000..7aab943446b --- /dev/null +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue'; +import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; +import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql'; +import { mockPipelineResponse } from './mock_data'; + +const defaultProvide = { + pipelineProjectPath: 'frog/amphibirama', + pipelineIid: '22', +}; + +describe('Pipeline graph wrapper', () => { + Vue.use(VueApollo); + + let wrapper; + const getAlert = () => wrapper.find(GlAlert); + const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + const getGraph = () => wrapper.find(PipelineGraph); + + const createComponent = ({ + apolloProvider, + data = {}, + provide = defaultProvide, + mountFn = shallowMount, + } = {}) => { + wrapper = mountFn(PipelineGraphWrapper, { + provide, + apolloProvider, + data() { + return { + ...data, + }; + }, + }); + }; + + const createComponentWithApollo = ( + getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse), + ) => { + const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]]; + + const apolloProvider = createMockApollo(requestHandlers); + createComponent({ apolloProvider }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when data is loading', () => { + it('displays the loading icon', () => { + createComponentWithApollo(); + expect(getLoadingIcon().exists()).toBe(true); + }); + + it('does not display the alert', () => { + createComponentWithApollo(); + expect(getAlert().exists()).toBe(false); + }); + + it('does not display the graph', () => { + createComponentWithApollo(); + expect(getGraph().exists()).toBe(false); + }); + }); + + describe('when data has loaded', () => { + beforeEach(async () => { + createComponentWithApollo(); + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + }); + + it('does not display the loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('does not display the alert', () => { + expect(getAlert().exists()).toBe(false); + }); + + it('displays the graph', () => { + expect(getGraph().exists()).toBe(true); + }); + }); + + describe('when there is an error', () => { + beforeEach(async () => { + createComponentWithApollo(jest.fn().mockRejectedValue(new Error('GraphQL error'))); + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + }); + + it('does not display the loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('displays the alert', () => { + expect(getAlert().exists()).toBe(true); + }); + + it('does not display the graph', () => { + expect(getGraph().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/utils_spec.js b/spec/frontend/registry/explorer/utils_spec.js index 0cd4a1cec29..7a5d6958a09 100644 --- a/spec/frontend/registry/explorer/utils_spec.js +++ b/spec/frontend/registry/explorer/utils_spec.js @@ -8,6 +8,10 @@ describe('Utils', () => { id: 1, }; + beforeEach(() => { + window.gon.relative_url_root = null; + }); + it('returns the fetch url when no ending is passed', () => { expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json'); }); @@ -16,7 +20,7 @@ describe('Utils', () => { expect(pathGenerator(imageDetails, '/foo')).toBe('/foo/bar/registry/repository/1/tags/foo'); }); - it.each` + describe.each` path | name | result ${'foo/foo'} | ${''} | ${'/foo/foo/registry/repository/1/tags?format=json'} ${'foo/foo/foo'} | ${'foo'} | ${'/foo/foo/registry/repository/1/tags?format=json'} @@ -26,8 +30,15 @@ describe('Utils', () => { ${'foo/foo/baz/foo/bar'} | ${'foo/bar'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'} ${'baz/foo/foo'} | ${'foo'} | ${'/baz/foo/registry/repository/1/tags?format=json'} ${'baz/foo/bar'} | ${'foo'} | ${'/baz/foo/bar/registry/repository/1/tags?format=json'} - `('returns the correct path when path is $path and name is $name', ({ name, path, result }) => { - expect(pathGenerator({ id: 1, name, path })).toBe(result); + `('when path is $path and name is $name', ({ name, path, result }) => { + it('returns the correct value', () => { + expect(pathGenerator({ id: 1, name, path })).toBe(result); + }); + + it('produces a correct relative url', () => { + window.gon.relative_url_root = '/gitlab'; + expect(pathGenerator({ id: 1, name, path })).toBe(`/gitlab${result}`); + }); }); it('returns the url unchanged when imageDetails have no name', () => { diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5a29e7b5c6c..05387f06746 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -269,7 +269,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let!(:older_other_pipeline) { create(:ci_pipeline, project: project) } let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } - let!(:child_pipeline) { create(:ci_pipeline, project: project) } + let!(:child_pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) } let!(:other_pipeline) { create(:ci_pipeline, project: project) } @@ -755,8 +755,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is merge request' do + before do + stub_feature_flags(ci_mr_diff_variables: false) + end + let(:pipeline) do - create(:ci_pipeline, merge_request: merge_request) + create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) end let(:merge_request) do @@ -784,30 +788,32 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, - 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => pipeline.target_sha.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '', 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, - 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '', 'CI_MERGE_REQUEST_TITLE' => merge_request.title, 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), - 'CI_MERGE_REQUEST_EVENT_TYPE' => pipeline.merge_request_event_type.to_s) + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached') + expect(subject.to_hash.keys).not_to include( + %w[CI_MERGE_REQUEST_DIFF_ID + CI_MERGE_REQUEST_DIFF_BASE_SHA]) end - context 'when source project does not exist' do + context 'when feature flag ci_mr_diff_variables is enabled' do before do - merge_request.update_column(:source_project_id, nil) + stub_feature_flags(ci_mr_diff_variables: true) end - it 'does not expose source project related variables' do - expect(subject.to_hash.keys).not_to include( - %w[CI_MERGE_REQUEST_SOURCE_PROJECT_ID - CI_MERGE_REQUEST_SOURCE_PROJECT_PATH - CI_MERGE_REQUEST_SOURCE_PROJECT_URL - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME]) + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) end end @@ -834,6 +840,51 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') end end + + context 'with merged results' do + let(:pipeline) do + create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request) + end + + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha, + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result') + expect(subject.to_hash.keys).not_to include( + %w[CI_MERGE_REQUEST_DIFF_ID + CI_MERGE_REQUEST_DIFF_BASE_SHA]) + end + + context 'when feature flag ci_mr_diff_variables is enabled' do + before do + stub_feature_flags(ci_mr_diff_variables: true) + end + + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) + end + end + end end context 'when source is external pull request' do @@ -2754,13 +2805,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is child' do - let(:parent) { create(:ci_pipeline, project: pipeline.project) } - let(:sibling) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(parent, pipeline) - create_source_pipeline(parent, sibling) - end + let(:parent) { create(:ci_pipeline, project: project) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent) } + let!(:sibling) { create(:ci_pipeline, child_of: parent) } it 'returns parent sibling and self ids' do expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id) @@ -2768,11 +2815,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is parent' do - let(:child) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(pipeline, child) - end + let!(:child) { create(:ci_pipeline, child_of: pipeline) } it 'returns self and child ids' do expect(subject).to contain_exactly(pipeline.id, child.id) @@ -2780,17 +2823,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a child of a child pipeline' do - let(:ancestor) { create(:ci_pipeline, project: pipeline.project) } - let(:parent) { create(:ci_pipeline, project: pipeline.project) } - let(:cousin_parent) { create(:ci_pipeline, project: pipeline.project) } - let(:cousin) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(ancestor, parent) - create_source_pipeline(ancestor, cousin_parent) - create_source_pipeline(parent, pipeline) - create_source_pipeline(cousin_parent, cousin) - end + let(:ancestor) { create(:ci_pipeline, project: project) } + let!(:parent) { create(:ci_pipeline, child_of: ancestor) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent) } + let!(:cousin_parent) { create(:ci_pipeline, child_of: ancestor) } + let!(:cousin) { create(:ci_pipeline, child_of: cousin_parent) } it 'returns all family ids' do expect(subject).to contain_exactly( @@ -2800,11 +2837,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a triggered pipeline' do - let(:upstream) { create(:ci_pipeline, project: create(:project)) } - - before do - create_source_pipeline(upstream, pipeline) - end + let!(:upstream) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline)} it 'returns self id' do expect(subject).to contain_exactly(pipeline.id) @@ -2812,6 +2845,46 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#root_ancestor' do + subject { pipeline.root_ancestor } + + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when pipeline is child of child pipeline' do + let!(:root_ancestor) { create(:ci_pipeline, project: project) } + let!(:parent_pipeline) { create(:ci_pipeline, child_of: root_ancestor) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + + it 'returns the root ancestor' do + expect(subject).to eq(root_ancestor) + end + end + + context 'when pipeline is root ancestor' do + let!(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } + + it 'returns itself' do + expect(subject).to eq(pipeline) + end + end + + context 'when pipeline is standalone' do + it 'returns itself' do + expect(subject).to eq(pipeline) + end + end + + context 'when pipeline is multi-project downstream pipeline' do + let!(:upstream_pipeline) do + create(:ci_pipeline, project: create(:project), upstream_of: pipeline) + end + + it 'ignores cross project ancestors' do + expect(subject).to eq(pipeline) + end + end + end + describe '#stuck?' do before do create(:ci_build, :pending, pipeline: pipeline) @@ -3552,18 +3625,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#parent_pipeline' do let_it_be(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } - context 'when pipeline is triggered by a pipeline from the same project' do - let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) } - - before do - create(:ci_sources_pipeline, - source_pipeline: upstream_pipeline, - source_project: project, - pipeline: pipeline, - project: project) - end + let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) } it 'returns the parent pipeline' do expect(pipeline.parent_pipeline).to eq(upstream_pipeline) @@ -3575,15 +3639,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is triggered by a pipeline from another project' do - let(:upstream_pipeline) { create(:ci_pipeline) } - - before do - create(:ci_sources_pipeline, - source_pipeline: upstream_pipeline, - source_project: upstream_pipeline.project, - pipeline: pipeline, - project: project) - end + let(:pipeline) { create(:ci_pipeline, project: project) } + let!(:upstream_pipeline) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline) } it 'returns nil' do expect(pipeline.parent_pipeline).to be_nil @@ -3595,6 +3652,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is not triggered by a pipeline' do + let_it_be(:pipeline) { create(:ci_pipeline) } + it 'returns nil' do expect(pipeline.parent_pipeline).to be_nil end diff --git a/yarn.lock b/yarn.lock index 245586f1dff..5d20efdc21d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -866,10 +866,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.175.0.tgz#734f341784af1cd1d62d160a17bcdfb61ff7b04d" integrity sha512-gXpc87TGSXIzfAr4QER1Qw1v3P47pBO6BXkma52blgwXVmcFNe3nhQzqsqt66wKNzrIrk3lAcB4GUyPHbPVXpg== -"@gitlab/ui@24.1.0": - version "24.1.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-24.1.0.tgz#931b23c98e239ce6855aea39c68fb1d4941f2997" - integrity sha512-XpyFuz/JlMsOCyxYYvWoXWjTx3xZFhS68z6KkEVE+K7hh5IWPVPQWw5swD6dlZeQO4OfbQXGb7FAqafqZoNoCQ== +"@gitlab/ui@24.2.0": + version "24.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-24.2.0.tgz#6cc8d071b309f8472e7c86fef4622a07c708ec13" + integrity sha512-fwmO/uGcOsM/BQ23pLX8ymqeeuYvX2MbyHvfa9gu/LVY1PUimrFIUZ8ipYBbYlFm+Dn+1YEOQXJ4zgVAOYEyig== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |