summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-27 15:09:34 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-27 15:09:34 +0000
commit4c39dd11dcbdab4fdd9424a62320a1fc773c2918 (patch)
treeb3bc6139fdc8d1e3254304222f06f82821c5aa64
parent95ff19a65c5236863e4c7c7e198bfc1e2fa70f07 (diff)
downloadgitlab-ce-4c39dd11dcbdab4fdd9424a62320a1fc773c2918.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js38
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue79
-rw-r--r--app/assets/javascripts/packages/details/components/package_files.vue86
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue89
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue13
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql54
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js31
-rw-r--r--app/assets/javascripts/registry/explorer/utils.js11
-rw-r--r--app/models/ci/pipeline.rb49
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/views/projects/pipelines/show.html.haml2
-rw-r--r--changelogs/unreleased/288752-registry-details-relative-url-fix.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-23-0.yml5
-rw-r--r--config/feature_flags/development/ci_mr_diff_variables.yml8
-rw-r--r--config/feature_flags/development/ci_root_ancestor_for_pipeline_family.yml8
-rw-r--r--locale/gitlab.pot3
-rw-r--r--package.json2
-rw-r--r--spec/factories/ci/pipelines.rb10
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js39
-rw-r--r--spec/frontend/packages/details/components/app_spec.js30
-rw-r--r--spec/frontend/packages/details/components/package_files_spec.js99
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js86
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js111
-rw-r--r--spec/frontend/registry/explorer/utils_spec.js17
-rw-r--r--spec/models/ci/pipeline_spec.rb181
-rw-r--r--yarn.lock8
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"