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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-13 12:08:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-13 12:08:27 +0300
commit15ae4a8da83661f2b714d804721001a53b354d28 (patch)
tree91080b2b969a66857d78fb9008c1d0c367132a8d
parent8f71e69fdbb65d2cf95cf16ef5a0add0919edb45 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/clusters/forms/components/integration_form.vue4
-rw-r--r--app/assets/javascripts/emoji/index.js131
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js34
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js14
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue8
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/diffs.scss (renamed from app/assets/stylesheets/pages/diff.scss)123
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss96
-rw-r--r--app/assets/stylesheets/pages/groups.scss72
-rw-r--r--app/assets/stylesheets/pages/projects.scss95
-rw-r--r--app/controllers/projects/jobs_controller.rb48
-rw-r--r--app/models/ci/bridge.rb30
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/terraform/state.rb55
-rw-r--r--app/policies/ci/bridge_policy.rb12
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/services/ci/play_bridge_service.rb14
-rw-r--r--app/services/ci/play_build_service.rb4
-rw-r--r--app/services/ci/play_manual_stage_service.rb8
-rw-r--r--app/uploaders/terraform/versioned_state_uploader.rb14
-rw-r--r--app/views/layouts/nav/_classification_level_banner.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml12
-rw-r--r--app/views/projects/diffs/_warning.html.haml23
-rw-r--r--app/views/projects/merge_requests/show.html.haml1
-rw-r--r--app/views/shared/members/_access_request_links.html.haml6
-rw-r--r--changelogs/unreleased/233694-replace-bootstrap-alerts-in-app-views-projects-diffs-_warning-html.yml5
-rw-r--r--changelogs/unreleased/235108-migrate-terraform-states-to-versioniong.yml5
-rw-r--r--changelogs/unreleased/eread-migrate-tooltip-time-track-collapsed.yml5
-rw-r--r--changelogs/unreleased/improve-gfm-ac-emoji.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/feature_flags/development/ci_manual_bridges.yml7
-rw-r--r--config/initializers/sprockets.rb2
-rw-r--r--db/post_migrate/20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb18
-rw-r--r--db/schema_migrations/202009290521381
-rw-r--r--doc/articles/artifactory_and_gitlab/index.md5
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ce/index.md5
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ee/index.md5
-rw-r--r--doc/articles/how_to_install_git/index.md5
-rw-r--r--doc/articles/index.md5
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/index.md5
-rw-r--r--doc/articles/numerous_undo_possibilities_in_git/index.md5
-rw-r--r--doc/articles/openshift_and_gitlab/index.md3
-rw-r--r--doc/articles/runner_autoscale_aws/index.md5
-rw-r--r--doc/ci/yaml/README.md13
-rw-r--r--doc/container_registry/README.md5
-rw-r--r--doc/container_registry/troubleshooting.md5
-rw-r--r--doc/development/documentation/styleguide.md5
-rw-r--r--doc/getting-started/subscription.md3
-rw-r--r--doc/git_hooks/git_hooks.md5
-rw-r--r--doc/hooks/custom_hooks.md7
-rw-r--r--doc/incoming_email/README.md5
-rw-r--r--doc/incoming_email/postfix.md5
-rw-r--r--doc/logs/logs.md5
-rw-r--r--doc/pages/README.md5
-rw-r--r--doc/pages/administration.md5
-rw-r--r--doc/pages/getting_started_part_one.md5
-rw-r--r--doc/pages/getting_started_part_three.md5
-rw-r--r--doc/pages/getting_started_part_two.md5
-rw-r--r--doc/profile/README.md5
-rw-r--r--doc/profile/preferences.md5
-rw-r--r--doc/profile/two_factor_authentication.md5
-rw-r--r--doc/user/admin_area/settings/index.md2
-rw-r--r--doc/web_hooks/web_hooks.md5
-rw-r--r--lib/gitlab/ci/config/entry/bridge.rb19
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--lib/gitlab/ci/status/bridge/action.rb12
-rw-r--r--lib/gitlab/ci/status/bridge/factory.rb5
-rw-r--r--lib/gitlab/ci/status/bridge/manual.rb12
-rw-r--r--lib/gitlab/ci/status/bridge/play.rb19
-rw-r--r--locale/gitlab.pot24
-rw-r--r--qa/qa/page/project/pipeline/show.rb15
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb109
-rwxr-xr-xscripts/lint-doc.sh4
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb45
-rw-r--r--spec/controllers/projects/pipelines/stages_controller_spec.rb11
-rw-r--r--spec/factories/ci/bridge.rb13
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/frontend/emoji/emoji_spec.js156
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js59
-rw-r--r--spec/frontend/helpers/emoji.js66
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js11
-rw-r--r--spec/graphql/types/snippet_type_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb61
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb46
-rw-r--r--spec/models/ci/bridge_spec.rb91
-rw-r--r--spec/models/terraform/state_spec.rb35
-rw-r--r--spec/policies/ci/bridge_policy_spec.rb39
-rw-r--r--spec/services/ci/play_bridge_service_spec.rb56
-rw-r--r--spec/services/ci/play_manual_stage_service_spec.rb42
-rw-r--r--spec/uploaders/terraform/versioned_state_uploader_spec.rb20
101 files changed, 1531 insertions, 603 deletions
diff --git a/Gemfile b/Gemfile
index d426ad42403..89c04e17ed9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
gem 'rack-proxy', '~> 0.6.0'
gem 'sassc-rails', '~> 2.1.0'
-gem 'terser', '~> 1.0'
+gem 'gitlab-terser', '1.0.1.1'
gem 'addressable', '~> 2.7'
gem 'font-awesome-rails', '~> 4.7'
diff --git a/Gemfile.lock b/Gemfile.lock
index 97aa43db2a2..8699ec0a21d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -452,6 +452,8 @@ GEM
rubocop-performance (~> 1.5.2)
rubocop-rails (~> 2.5)
rubocop-rspec (~> 1.36)
+ gitlab-terser (1.0.1.1)
+ execjs (>= 0.3.0, < 3)
gitlab_chronic_duration (0.10.6.2)
numerizer (~> 0.2)
gitlab_omniauth-ldap (2.1.1)
@@ -1130,8 +1132,6 @@ GEM
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
- terser (1.0.1)
- execjs (>= 0.3.0, < 3)
test-prof (0.12.0)
text (1.3.1)
thin (1.7.2)
@@ -1333,6 +1333,7 @@ DEPENDENCIES
gitlab-puma_worker_killer (~> 0.1.1.gitlab.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 4.3.0)
+ gitlab-terser (= 1.0.1.1)
gitlab_chronic_duration (~> 0.10.6.2)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
@@ -1482,7 +1483,6 @@ DEPENDENCIES
stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
- terser (~> 1.0)
test-prof (~> 0.12.0)
thin (~> 1.7.0)
timecop (~> 0.9.1)
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index bc69c02e21e..7055cd42978 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -572,7 +572,7 @@ export class AwardsHandler {
}
findMatchingEmojiElements(query) {
- const emojiMatches = this.emoji.searchEmoji(query).map(({ name }) => name);
+ const emojiMatches = this.emoji.searchEmoji(query, { match: 'fuzzy' }).map(({ name }) => name);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
diff --git a/app/assets/javascripts/clusters/forms/components/integration_form.vue b/app/assets/javascripts/clusters/forms/components/integration_form.vue
index 53e004b4fc0..f0dafa7ef53 100644
--- a/app/assets/javascripts/clusters/forms/components/integration_form.vue
+++ b/app/assets/javascripts/clusters/forms/components/integration_form.vue
@@ -24,10 +24,10 @@ export default {
},
inject: {
autoDevopsHelpPath: {
- type: String,
+ default: '',
},
externalEndpointHelpPath: {
- type: String,
+ default: '',
},
},
data() {
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 1bdc7b3a8b5..b03da311c43 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,4 +1,3 @@
-import { uniq } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils';
@@ -67,49 +66,111 @@ export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
+export function getValidEmojiUnicodeValues() {
+ return Object.values(emojiMap).map(({ e }) => e);
+}
+
+export function getValidEmojiDescriptions() {
+ return Object.values(emojiMap).map(({ d }) => d);
+}
+
/**
- * Search emoji by name or alias. Returns a normalized, deduplicated list of
- * names.
+ * Retrieves an emoji by name or alias.
*
- * Calling with an empty filter returns an empty array.
+ * Note: `initEmojiMap` must have been called and completed before this method
+ * can safely be called.
*
- * @param {String}
- * @returns {Array}
+ * @param {String} query The emoji name
+ * @param {Boolean} fallback If true, a fallback emoji will be returned if the
+ * named emoji does not exist. Defaults to false.
+ * @returns {Object} The matching emoji.
*/
-export function queryEmojiNames(filter) {
- const matches = fuzzaldrinPlus.filter(validEmojiNames, filter);
- return uniq(matches.map(name => normalizeEmojiName(name)));
+export function getEmoji(query, fallback = false) {
+ if (!emojiMap) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('The emoji map is uninitialized or initialization has not completed');
+ }
+
+ const lowercaseQuery = query.toLowerCase();
+ const name = normalizeEmojiName(lowercaseQuery);
+
+ if (name in emojiMap) {
+ return emojiMap[name];
+ }
+
+ if (fallback) {
+ return emojiMap.grey_question;
+ }
+
+ return null;
}
+const searchMatchers = {
+ fuzzy: (value, query) => fuzzaldrinPlus.score(value, query) > 0, // Fuzzy matching compares using a fuzzy matching library
+ contains: (value, query) => value.indexOf(query.toLowerCase()) >= 0, // Contains matching compares by indexOf
+ exact: (value, query) => value === query.toLowerCase(), // Exact matching compares by equality
+};
+
+const searchPredicates = {
+ name: (matcher, query) => emoji => matcher(emoji.name, query), // Search by name
+ alias: (matcher, query) => emoji => emoji.aliases.some(v => matcher(v, query)), // Search by alias
+ description: (matcher, query) => emoji => matcher(emoji.d, query), // Search by description
+ unicode: (matcher, query) => emoji => emoji.e === query, // Search by unicode value (always exact)
+};
+
/**
- * Searches emoji by name, alias, description, and unicode value and returns an
- * array of matches.
+ * Searches emoji by name, aliases, description, and unicode value and returns
+ * an array of matches.
+ *
+ * Behavior is undefined if `opts.fields` is empty or if `opts.match` is fuzzy
+ * and the query is empty.
*
* Note: `initEmojiMap` must have been called and completed before this method
* can safely be called.
*
- * @param {String} query The search query
- * @returns {Object[]} A list of emoji that match the query
+ * @param {String} query Search query.
+ * @param {Object} opts Search options (optional).
+ * @param {String[]} opts.fields Fields to search. Choices are 'name', 'alias',
+ * 'description', and 'unicode' (value). Default is all (four) fields.
+ * @param {String} opts.match Search method to use. Choices are 'exact',
+ * 'contains', or 'fuzzy'. All methods are case-insensitive. Exact matching (the
+ * default) compares by equality. Contains matching compares by indexOf. Fuzzy
+ * matching compares using a fuzzy matching library.
+ * @param {Boolean} opts.fallback If true, a fallback emoji will be returned if
+ * the result set is empty. Defaults to false.
+ * @returns {Object[]} A list of emoji that match the query.
*/
-export function searchEmoji(query) {
- if (!emojiMap)
+export function searchEmoji(query, opts) {
+ if (!emojiMap) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('The emoji map is uninitialized or initialization has not completed');
+ }
+
+ const {
+ fields = ['name', 'alias', 'description', 'unicode'],
+ match = 'exact',
+ fallback = false,
+ } = opts || {};
- const matches = s => fuzzaldrinPlus.score(s, query) > 0;
-
- // Search emoji
- return Object.values(emojiMap).filter(
- emoji =>
- // by name
- matches(emoji.name) ||
- // by alias
- emoji.aliases.some(matches) ||
- // by description
- matches(emoji.d) ||
- // by unicode value
- query === emoji.e,
+ // optimization for an exact match in name and alias
+ if (match === 'exact' && new Set([...fields, 'name', 'alias']).size === 2) {
+ const emoji = getEmoji(query, fallback);
+ return emoji ? [emoji] : [];
+ }
+
+ const matcher = searchMatchers[match] || searchMatchers.exact;
+ const predicates = fields.map(f => searchPredicates[f](matcher, query));
+
+ const results = Object.values(emojiMap).filter(emoji =>
+ predicates.some(predicate => predicate(emoji)),
);
+
+ // Fallback to question mark for unknown emojis
+ if (fallback && results.length === 0) {
+ return [emojiMap.grey_question];
+ }
+
+ return results;
}
let emojiCategoryMap;
@@ -136,16 +197,10 @@ export function getEmojiCategoryMap() {
}
export function getEmojiInfo(query) {
- let name = normalizeEmojiName(query);
- let emojiInfo = emojiMap[name];
-
- // Fallback to question mark for unknown emojis
- if (!emojiInfo) {
- name = 'grey_question';
- emojiInfo = emojiMap[name];
- }
-
- return { ...emojiInfo, name };
+ return searchEmoji(query, {
+ fields: ['name', 'alias'],
+ fallback: true,
+ })[0];
}
export function emojiFallbackImageSrc(inputName) {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 0329006c62a..5b604cc2a05 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -191,8 +191,7 @@ class GfmAutoComplete {
}
return tmpl;
},
- // eslint-disable-next-line no-template-curly-in-string
- insertTpl: ':${name}:',
+ insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction,
skipSpecialCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
callbacks: {
@@ -612,12 +611,7 @@ class GfmAutoComplete {
} else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
- Emoji.initEmojiMap()
- .then(() => {
- this.loadData($input, at, Emoji.getValidEmojiNames());
- GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
- })
- .catch(() => {});
+ this.loadEmojiData($input, at).catch(() => {});
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then(data => {
@@ -640,6 +634,18 @@ class GfmAutoComplete {
return $input.trigger('keyup');
}
+ async loadEmojiData($input, at) {
+ await Emoji.initEmojiMap();
+
+ this.loadData($input, at, [
+ ...Emoji.getValidEmojiNames(),
+ ...Emoji.getValidEmojiDescriptions(),
+ ...Emoji.getValidEmojiUnicodeValues(),
+ ]);
+
+ GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
+ }
+
clearCache() {
this.cachedData = {};
}
@@ -708,12 +714,16 @@ GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
// Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = {
+ insertTemplateFunction(value) {
+ const { name = value.name } = Emoji.searchEmoji(value.name, { match: 'contains' })[0] || {};
+ return `:${name}:`;
+ },
templateFunction(name) {
// glEmojiTag helper is loaded on-demand in fetchData()
- if (GfmAutoComplete.glEmojiTag) {
- return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
- }
- return `<li>${name}</li>`;
+ if (!GfmAutoComplete.glEmojiTag) return `<li>${name}</li>`;
+
+ const emoji = Emoji.searchEmoji(name, { match: 'contains' })[0];
+ return `<li>${name} ${GfmAutoComplete.glEmojiTag(emoji?.name || name)}</li>`;
},
};
// Team Members
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 993d51370ec..1a4ecc12f01 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,4 +1,5 @@
export const BYTES_IN_KIB = 1024;
+export const BYTES_IN_KB = 1000;
export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index bc87232f40b..2424d6cbf3b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,4 +1,4 @@
-import { BYTES_IN_KIB } from './constants';
+import { BYTES_IN_KIB, BYTES_IN_KB } from './constants';
import { sprintf, __ } from '~/locale';
/**
@@ -35,6 +35,18 @@ export function formatRelevantDigits(number) {
}
/**
+ * Utility function that calculates KB of the given bytes.
+ * Note: This method calculates KiloBytes as opposed to
+ * Kibibytes. For Kibibytes, bytesToKiB should be used.
+ *
+ * @param {Number} number bytes
+ * @return {Number} KiB
+ */
+export function bytesToKB(number) {
+ return number / BYTES_IN_KB;
+}
+
+/**
* Utility function that calculates KiB of the given bytes.
*
* @param {Number} number bytes
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index 533065b2d4d..dfce1cb75d3 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -70,14 +70,18 @@ export default {
</script>
<template>
- <div :data-for="name" class="project-feature-controls">
+ <div
+ :data-for="name"
+ class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0"
+ >
<input v-if="name" :name="name" :value="value" type="hidden" />
<project-feature-toggle
+ class="gl-flex-grow-0 gl-mr-3"
:value="featureEnabled"
:disabled-input="disabledInput"
@change="toggleFeature"
/>
- <div class="select-wrapper">
+ <div class="select-wrapper gl-flex-fill-1">
<select
:disabled="displaySelectInput"
class="form-control project-repo-select select-control"
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a95f0af46cd..e19afe67789 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -292,14 +292,16 @@ export default {
<template>
<div>
- <div class="project-visibility-setting">
+ <div
+ class="project-visibility-setting gl-border-1 gl-border-solid gl-border-gray-100 gl-py-3 gl-px-7 gl-sm-pr-5 gl-sm-pl-5"
+ >
<project-setting-row
ref="project-visibility-settings"
:help-path="visibilityHelpPath"
:label="s__('ProjectSettings|Project visibility')"
>
- <div class="project-feature-controls">
- <div class="select-wrapper">
+ <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
+ <div class="select-wrapper gl-flex-fill-1">
<select
v-model="visibilityLevel"
:disabled="!canChangeVisibilityLevel"
@@ -327,7 +329,7 @@ export default {
</div>
</div>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
- <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="request-access">
+ <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="gl-line-height-28">
<input
:value="requestAccessEnabled"
type="hidden"
@@ -338,7 +340,10 @@ export default {
</label>
</project-setting-row>
</div>
- <div :class="{ 'highlight-changes': highlightChangesClass }" class="project-feature-settings">
+ <div
+ :class="{ 'highlight-changes': highlightChangesClass }"
+ class="gl-border-1 gl-border-solid gl-border-t-none gl-border-gray-100 gl-mb-5 gl-py-3 gl-px-7 gl-sm-pr-5 gl-sm-pl-5 gl-bg-gray-10"
+ >
<project-setting-row
ref="issues-settings"
:label="s__('ProjectSettings|Issues')"
@@ -361,7 +366,7 @@ export default {
name="project[project_feature_attributes][repository_access_level]"
/>
</project-setting-row>
- <div class="project-feature-setting-group">
+ <div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row
ref="merge-request-settings"
:label="s__('ProjectSettings|Merge requests')"
@@ -516,8 +521,8 @@ export default {
)
"
>
- <div class="project-feature-controls">
- <div class="select-wrapper">
+ <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
+ <div class="select-wrapper gl-flex-fill-1">
<select
v-model="metricsDashboardAccessLevel"
:disabled="metricsOptionsDropdownEnabled"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 9f7fe85fb0d..7aee2573ce1 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -126,7 +126,7 @@ export default {
};
</script>
<template>
- <div class="ci-job-component">
+ <div class="ci-job-component" data-qa-selector="job_item_container">
<gl-link
v-if="status.has_details"
v-gl-tooltip="{ boundary, placement: 'bottom' }"
@@ -156,6 +156,7 @@ export default {
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
+ data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index bc2319c0f36..9d72bf4394e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,7 +1,6 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
@@ -9,7 +8,7 @@ export default {
GlIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
showComparisonState: {
@@ -97,14 +96,7 @@ export default {
</script>
<template>
- <div
- v-tooltip
- :title="tooltipText"
- class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- >
+ <div v-gl-tooltip:body.viewport.left :title="tooltipText" class="sidebar-collapsed-icon">
<gl-icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 29d4516bece..861661d3519 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -59,7 +59,7 @@ export default {
</script>
<template>
- <label class="toggle-wrapper">
+ <label class="gl-mt-2">
<input v-if="name" :name="name" :value="value" type="hidden" />
<button
type="button"
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index eb602651d43..d3ab4be925b 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -8,7 +8,6 @@
@import './pages/commits';
@import './pages/deploy_keys';
@import './pages/detail_page';
-@import './pages/diff';
@import './pages/editor';
@import './pages/environment_logs';
@import './pages/error_list';
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index f875420b9c9..e40b95cdce6 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -70,3 +70,4 @@
@import 'framework/spinner';
@import 'framework/card';
@import 'framework/editor-lite';
+@import 'framework/diffs';
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/framework/diffs.scss
index 7d622dda070..c0a2350d080 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -267,6 +267,7 @@
}
}
}
+
//.view.swipe
.view.onion-skin {
.onion-skin-frame {
@@ -335,6 +336,7 @@
}
}
}
+
//.view.onion-skin
}
@@ -961,15 +963,13 @@ table.code {
.frame.click-to-comment,
.btn-transparent.image-diff-overlay-add-comment {
position: relative;
- cursor: image-url('illustrations/image_comment_light_cursor.svg')
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ cursor: image-url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
// Retina cursor
// scss-lint:disable DuplicateProperty
cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x,
- image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
.comment-indicator {
@@ -1078,85 +1078,6 @@ table.code {
position: relative;
}
-.diff-tree-list {
- position: -webkit-sticky;
- position: sticky;
- $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
- top: $top-pos;
- max-height: calc(100vh - #{$top-pos});
- z-index: 202;
-
- .with-system-header & {
- top: $top-pos + $system-header-height;
- }
-
- .with-system-header.with-performance-bar & {
- top: $top-pos + $system-header-height + $performance-bar-height;
- }
-
- .with-performance-bar & {
- $performance-bar-top-pos: $performance-bar-height + $top-pos;
- top: $performance-bar-top-pos;
- max-height: calc(100vh - #{$performance-bar-top-pos});
- }
-
- .drag-handle {
- bottom: 16px;
- transform: translateX(10px);
- }
-}
-
-.diff-files-holder {
- flex: 1;
- min-width: 0;
- z-index: 201;
-}
-
-.compare-versions-container {
- min-width: 0;
-}
-
-.tree-list-holder {
- height: 100%;
-
- .file-row {
- margin-left: 0;
- margin-right: 0;
- }
-}
-
-.tree-list-scroll {
- max-height: 100%;
- padding-bottom: $grid-size;
- overflow-y: scroll;
- overflow-x: auto;
-}
-
-.tree-list-search {
- flex: 0 0 34px;
-
- .form-control {
- padding-left: 30px;
- }
-}
-
-.tree-list-icon {
- top: 50%;
- left: 10px;
- transform: translateY(-50%);
-
- &,
- svg {
- fill: $gl-text-color-tertiary;
- }
-}
-
-.tree-list-clear-icon {
- right: 10px;
- left: auto;
- line-height: 0;
-}
-
.discussion-collapsible {
margin: 0 $gl-padding $gl-padding 71px;
@@ -1172,30 +1093,6 @@ table.code {
}
}
-@media (max-width: map-get($grid-breakpoints, md)-1) {
- .diffs .files {
- @include fixed-width-container;
- flex-direction: column;
-
- .diff-tree-list {
- position: relative;
- top: 0;
- // !important is required to override inline styles of resizable sidebar
- width: 100% !important;
- }
-
- .tree-list-holder {
- max-height: calc(50px + 50vh);
- padding-right: 0;
- }
- }
-
- .discussion-collapsible {
- margin: $gl-padding;
- margin-top: 0;
- }
-}
-
.image-diff-overlay,
.image-diff-overlay-add-comment {
top: 0;
@@ -1218,3 +1115,15 @@ table.code {
display: none;
}
}
+
+@media (max-width: map-get($grid-breakpoints, md)-1) {
+ .diffs .files {
+ @include fixed-width-container;
+ flex-direction: column;
+ }
+
+ .discussion-collapsible {
+ margin: $gl-padding;
+ margin-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
new file mode 100644
index 00000000000..5553dffac05
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -0,0 +1,96 @@
+@import 'mixins_and_variables_and_functions';
+
+.compare-versions-container {
+ min-width: 0;
+}
+
+.diff-files-holder {
+ flex: 1;
+ min-width: 0;
+ z-index: 201;
+}
+
+.diff-tree-list {
+ position: -webkit-sticky;
+ position: sticky;
+ $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
+ top: $top-pos;
+ max-height: calc(100vh - #{$top-pos});
+ z-index: 202;
+
+ .with-system-header & {
+ top: $top-pos + $system-header-height;
+ }
+
+ .with-system-header.with-performance-bar & {
+ top: $top-pos + $system-header-height + $performance-bar-height;
+ }
+
+ .with-performance-bar & {
+ $performance-bar-top-pos: $performance-bar-height + $top-pos;
+ top: $performance-bar-top-pos;
+ max-height: calc(100vh - #{$performance-bar-top-pos});
+ }
+
+ .drag-handle {
+ bottom: 16px;
+ transform: translateX(10px);
+ }
+}
+
+.tree-list-holder {
+ height: 100%;
+
+ .file-row {
+ margin-left: 0;
+ margin-right: 0;
+ }
+}
+
+.tree-list-scroll {
+ max-height: 100%;
+ padding-bottom: $grid-size;
+ overflow-y: scroll;
+ overflow-x: auto;
+}
+
+.tree-list-search {
+ flex: 0 0 34px;
+
+ .form-control {
+ padding-left: 30px;
+ }
+}
+
+.tree-list-icon {
+ top: 50%;
+ left: 10px;
+ transform: translateY(-50%);
+
+ &,
+ svg {
+ fill: var(--gray-400, $gray-400);
+ }
+}
+
+.tree-list-clear-icon {
+ right: 10px;
+ left: auto;
+ line-height: 0;
+}
+
+@media (max-width: map-get($grid-breakpoints, md)-1) {
+ .diffs .files {
+ .diff-tree-list {
+ position: relative;
+ top: 0;
+ // !important is required to override inline styles of resizable sidebar
+ width: 100% !important;
+ }
+
+ .tree-list-holder {
+ max-height: calc(50px + 50vh);
+ padding-right: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 69fd094f83b..ee4f74882a1 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -95,6 +95,78 @@
}
}
+.group-home-panel {
+ margin-top: $gl-padding;
+ margin-bottom: $gl-padding;
+
+ .home-panel-avatar {
+ width: $home-panel-title-row-height;
+ height: $home-panel-title-row-height;
+ flex-shrink: 0;
+ flex-basis: $home-panel-title-row-height;
+ }
+
+ .home-panel-title {
+ font-size: 20px;
+ line-height: $gl-line-height-24;
+ font-weight: bold;
+
+ .icon {
+ vertical-align: -1px;
+ }
+
+ .home-panel-topic-list {
+ font-size: $gl-font-size;
+ font-weight: $gl-font-weight-normal;
+
+ .icon {
+ position: relative;
+ top: 3px;
+ margin-right: $gl-padding-4;
+ }
+ }
+ }
+
+ .home-panel-title-row {
+ @include media-breakpoint-down(sm) {
+ .home-panel-avatar {
+ width: $home-panel-avatar-mobile-size;
+ height: $home-panel-avatar-mobile-size;
+ flex-basis: $home-panel-avatar-mobile-size;
+
+ .avatar {
+ font-size: 20px;
+ line-height: 46px;
+ }
+ }
+
+ .home-panel-title {
+ margin-top: 4px;
+ margin-bottom: 2px;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size-large;
+ }
+
+ .home-panel-topic-list,
+ .home-panel-metadata {
+ font-size: $gl-font-size-small;
+ }
+ }
+ }
+
+ .home-panel-metadata {
+ font-weight: normal;
+ font-size: 14px;
+ line-height: $gl-btn-line-height;
+ }
+
+ .home-panel-description {
+ @include media-breakpoint-up(md) {
+ font-size: $gl-font-size-large;
+ }
+ }
+}
+
.home-panel-buttons {
.home-panel-action-button {
vertical-align: top;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 05ade210153..938d8d34717 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1,11 +1,3 @@
-.alert_holder {
- margin: -16px;
-
- .alert-link {
- font-weight: $gl-font-weight-normal;
- }
-}
-
.new_project,
.edit-project,
.import-project {
@@ -67,38 +59,7 @@
}
}
-.classification-label {
- background-color: $red-500;
-}
-
-.toggle-wrapper {
- margin-top: 5px;
-}
-
-.project-feature-row > .toggle-wrapper {
- margin: 10px 0;
-}
-
-.project-visibility-setting,
-.project-feature-settings {
- border: 1px solid $border-color;
- padding: 10px 32px;
-
- @include media-breakpoint-down(xs) {
- padding: 10px 20px;
- }
-}
-
-.project-visibility-setting .request-access {
- line-height: 2;
-}
-
-.project-feature-settings {
- background: $gray-lighter;
- border-top: 0;
- margin-bottom: 16px;
-}
-
+// INFO Scoped to project_feature_setting and settings_panel components in app/assets/javascripts/pages/projects/shared/permissions/components
.project-repo-select {
transition: background 2s ease-out;
@@ -113,63 +74,31 @@
}
}
+// INFO Scoped to project_feature_setting and settings_panel components in app/assets/javascripts/pages/projects/shared/permissions/components
.project-feature-controls {
- display: flex;
- align-items: center;
- margin: $gl-padding-8 0;
max-width: 432px;
-
- .toggle-wrapper {
- flex: 0;
- margin-right: 10px;
- }
-
- .select-wrapper {
- flex: 1;
- }
}
+// INFO Scoped to settings_panel component in app/assets/javascripts/pages/projects/shared/permissions/components
.project-feature-setting-group {
- padding-left: 32px;
-
.project-feature-controls {
max-width: 400px;
}
-
- @include media-breakpoint-down(xs) {
- padding-left: 20px;
- }
}
-.group-home-panel,
.project-home-panel {
- margin-top: $gl-padding;
- margin-bottom: $gl-padding;
-
.home-panel-avatar {
- width: $home-panel-title-row-height;
- height: $home-panel-title-row-height;
- flex-shrink: 0;
flex-basis: $home-panel-title-row-height;
}
.home-panel-title {
- font-size: 20px;
- line-height: $gl-line-height-24;
- font-weight: bold;
-
.icon {
vertical-align: -1px;
}
.home-panel-topic-list {
- font-size: $gl-font-size;
- font-weight: $gl-font-weight-normal;
-
.icon {
- position: relative;
top: 3px;
- margin-right: $gl-padding-4;
}
}
}
@@ -201,24 +130,6 @@
}
}
- .home-panel-metadata {
- font-weight: normal;
- font-size: 14px;
- line-height: $gl-btn-line-height;
-
- .home-panel-license {
- .btn {
- line-height: 0;
- border-width: 0;
- }
- }
-
- .access-request-link {
- padding-left: $gl-padding-8;
- border-left: 1px solid $gl-text-color-secondary;
- }
- }
-
.home-panel-description {
@include media-breakpoint-up(md) {
font-size: $gl-font-size-large;
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 2a7dbbb43d7..3ceb60a6aef 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
include ContinueParams
- before_action :build, except: [:index]
+ before_action :find_job_as_build, except: [:index, :play]
+ before_action :find_job_as_processable, only: [:play]
before_action :authorize_read_build!
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :erase]
@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def trace
- build.trace.read do |stream|
+ @build.trace.read do |stream|
respond_to do |format|
format.json do
- build.trace.being_watched!
+ @build.trace.being_watched!
build_trace = Ci::BuildTrace.new(
build: @build,
@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController
def play
return respond_422 unless @build.playable?
- build = @build.play(current_user, play_params[:job_variables_attributes])
- redirect_to build_path(build)
+ job = @build.play(current_user, play_params[:job_variables_attributes])
+
+ if job.is_a?(Ci::Bridge)
+ redirect_to pipeline_path(job.pipeline)
+ else
+ redirect_to build_path(job)
+ end
end
def cancel
@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController
send_params: raw_send_params,
redirect_params: raw_redirect_params)
else
- build.trace.read do |stream|
+ @build.trace.read do |stream|
if stream.file?
workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController
private
def authorize_update_build!
- return access_denied! unless can?(current_user, :update_build, build)
+ return access_denied! unless can?(current_user, :update_build, @build)
end
def authorize_erase_build!
- return access_denied! unless can?(current_user, :erase_build, build)
+ return access_denied! unless can?(current_user, :erase_build, @build)
end
def authorize_use_build_terminal!
- return access_denied! unless can?(current_user, :create_build_terminal, build)
+ return access_denied! unless can?(current_user, :create_build_terminal, @build)
end
def authorize_create_proxy_build!
- return access_denied! unless can?(current_user, :create_build_service_proxy, build)
+ return access_denied! unless can?(current_user, :create_build_service_proxy, @build)
end
def verify_api_request!
@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController
end
def trace_artifact_file
- @trace_artifact_file ||= build.job_artifacts_trace&.file
+ @trace_artifact_file ||= @build.job_artifacts_trace&.file
end
- def build
- @build ||= project.builds.find(params[:id])
+ def find_job_as_build
+ @build = project.builds.find(params[:id])
.present(current_user: current_user)
end
+ def find_job_as_processable
+ if ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
+ @build = project.processables.find(params[:id])
+ else
+ find_job_as_build
+ end
+ end
+
def build_path(build)
project_job_path(build.project, build)
end
@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def build_service_specification
- build.service_specification(service: params['service'],
- port: params['port'],
- path: params['path'],
- subprotocols: proxy_subprotocol)
+ @build.service_specification(service: params['service'],
+ port: params['port'],
+ path: params['path'],
+ subprotocols: proxy_subprotocol)
end
def proxy_subprotocol
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 1697067f633..2e725e0baff 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do
- after_transition created: :pending do |bridge|
+ after_transition [:created, :manual] => :pending do |bridge|
next unless bridge.downstream_project
bridge.run_after_commit do
@@ -46,6 +46,10 @@ module Ci
event :scheduled do
transition all => :scheduled
end
+
+ event :actionize do
+ transition created: :manual
+ end
end
def self.retry(bridge, current_user)
@@ -126,9 +130,27 @@ module Ci
false
end
+ def playable?
+ return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
+
+ action? && !archived? && manual?
+ end
+
def action?
- false
+ return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
+
+ %w[manual].include?(self.when)
+ end
+
+ # rubocop: disable CodeReuse/ServiceClass
+ # We don't need it but we are taking `job_variables_attributes` parameter
+ # to make it consistent with `Ci::Build#play` method.
+ def play(current_user, job_variables_attributes = nil)
+ Ci::PlayBridgeService
+ .new(project, current_user)
+ .execute(self)
end
+ # rubocop: enable CodeReuse/ServiceClass
def artifacts?
false
@@ -185,6 +207,10 @@ module Ci
[]
end
+ def target_revision_ref
+ downstream_pipeline_params.dig(:target_revision, :ref)
+ end
+
private
def cross_project_params
diff --git a/app/models/project.rb b/app/models/project.rb
index 71816dfb9ba..9fa93d9b4e4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -298,6 +298,7 @@ class Project < ApplicationRecord
# bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 2ff2e3d66c0..9d88db27449 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -37,7 +37,11 @@ module Terraform
end
def latest_file
- versioning_enabled ? latest_version&.file : file
+ if versioning_enabled?
+ latest_version&.file
+ else
+ latest_version&.file || file
+ end
end
def locked?
@@ -46,13 +50,56 @@ module Terraform
def update_file!(data, version:)
if versioning_enabled?
- new_version = versions.build(version: version)
- new_version.assign_attributes(created_by_user: locked_by_user, file: data)
- new_version.save!
+ create_new_version!(data: data, version: version)
+ elsif latest_version.present?
+ migrate_legacy_version!(data: data, version: version)
else
self.file = data
save!
end
end
+
+ private
+
+ ##
+ # If a Terraform state was created before versioning support was
+ # introduced, it will have a single version record whose file
+ # uses a legacy naming scheme in object storage. To update
+ # these states and versions to use the new behaviour, we must do
+ # the following when creating the next version:
+ #
+ # * Read the current, non-versioned file from the old location.
+ # * Update the :versioning_enabled flag, which determines the
+ # naming scheme
+ # * Resave the existing file with the updated name and location,
+ # using a version number one prior to the new version
+ # * Create the new version as normal
+ #
+ # This migration only needs to happen once for each state, from
+ # then on the state will behave as if it was always versioned.
+ #
+ # The code can be removed in the next major version (14.0), after
+ # which any states that haven't been migrated will need to be
+ # recreated: https://gitlab.com/gitlab-org/gitlab/-/issues/258960
+ def migrate_legacy_version!(data:, version:)
+ current_file = latest_version.file.read
+ current_version = parse_serial(current_file) || version - 1
+
+ update!(versioning_enabled: true)
+
+ reload_latest_version.update!(version: current_version, file: CarrierWaveStringFile.new(current_file))
+ create_new_version!(data: data, version: version)
+ end
+
+ def create_new_version!(data:, version:)
+ new_version = versions.build(version: version, created_by_user: locked_by_user)
+ new_version.assign_attributes(file: data)
+ new_version.save!
+ end
+
+ def parse_serial(file)
+ Gitlab::Json.parse(file)["serial"]
+ rescue JSON::ParserError
+ end
end
end
diff --git a/app/policies/ci/bridge_policy.rb b/app/policies/ci/bridge_policy.rb
new file mode 100644
index 00000000000..37a07ea8aaf
--- /dev/null
+++ b/app/policies/ci/bridge_policy.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Ci
+ class BridgePolicy < CommitStatusPolicy
+ condition(:can_update_downstream_branch) do
+ ::Gitlab::UserAccess.new(@user, container: @subject.downstream_project)
+ .can_update_branch?(@subject.target_revision_ref)
+ end
+
+ rule { can_update_downstream_branch }.enable :play_job
+ end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index b3950c6a0e3..3efc07421e4 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -60,6 +60,8 @@ module Ci
rule { can?(:update_build) & terminal }.enable :create_build_terminal
+ rule { can?(:update_build) }.enable :play_job
+
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal
enable :update_web_ide_terminal
diff --git a/app/services/ci/play_bridge_service.rb b/app/services/ci/play_bridge_service.rb
new file mode 100644
index 00000000000..70c4a8e6136
--- /dev/null
+++ b/app/services/ci/play_bridge_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ci
+ class PlayBridgeService < ::BaseService
+ def execute(bridge)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, bridge)
+
+ bridge.tap do |bridge|
+ bridge.user = current_user
+ bridge.enqueue!
+ end
+ end
+ end
+end
diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb
index 9f922ffde81..6adeca624a8 100644
--- a/app/services/ci/play_build_service.rb
+++ b/app/services/ci/play_build_service.rb
@@ -3,9 +3,7 @@
module Ci
class PlayBuildService < ::BaseService
def execute(build, job_variables_attributes = nil)
- unless can?(current_user, :update_build, build)
- raise Gitlab::Access::AccessDeniedError
- end
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build)
# Try to enqueue the build, otherwise create a duplicate.
#
diff --git a/app/services/ci/play_manual_stage_service.rb b/app/services/ci/play_manual_stage_service.rb
index 2497fc52e6b..c6fa7803e52 100644
--- a/app/services/ci/play_manual_stage_service.rb
+++ b/app/services/ci/play_manual_stage_service.rb
@@ -9,12 +9,12 @@ module Ci
end
def execute(stage)
- stage.builds.manual.each do |build|
- next unless build.playable?
+ stage.processables.manual.each do |processable|
+ next unless processable.playable?
- build.play(current_user)
+ processable.play(current_user)
rescue Gitlab::Access::AccessDeniedError
- logger.error(message: 'Unable to play manual action', build_id: build.id)
+ logger.error(message: 'Unable to play manual action', processable_id: processable.id)
end
end
diff --git a/app/uploaders/terraform/versioned_state_uploader.rb b/app/uploaders/terraform/versioned_state_uploader.rb
index be07993da0f..e50ab6c7dc6 100644
--- a/app/uploaders/terraform/versioned_state_uploader.rb
+++ b/app/uploaders/terraform/versioned_state_uploader.rb
@@ -2,12 +2,22 @@
module Terraform
class VersionedStateUploader < StateUploader
+ delegate :terraform_state, to: :model
+
def filename
- "#{model.version}.tfstate"
+ if terraform_state.versioning_enabled?
+ "#{model.version}.tfstate"
+ else
+ "#{model.uuid}.tfstate"
+ end
end
def store_dir
- Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
+ if terraform_state.versioning_enabled?
+ Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
+ else
+ project_id.to_s
+ end
end
end
end
diff --git a/app/views/layouts/nav/_classification_level_banner.html.haml b/app/views/layouts/nav/_classification_level_banner.html.haml
index cc4caf079b8..d76fb50aa0b 100644
--- a/app/views/layouts/nav/_classification_level_banner.html.haml
+++ b/app/views/layouts/nav/_classification_level_banner.html.haml
@@ -1,5 +1,5 @@
- if ::Gitlab::ExternalAuthorization.enabled? && @project
= content_for :header_content do
- %span.badge.color-label.classification-label.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') }
+ %span.badge.color-label.gl-bg-red-500.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') }
= sprite_icon('lock-open', size: 8, css_class: 'inline')
= @project.external_authorization_classification_label
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 94a2bdb3bcb..9f4496e7a13 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,19 +3,19 @@
- max_project_topic_length = 15
- emails_disabled = @project.emails_disabled?
-.project-home-panel.js-show-on-project-root{ class: [("empty-project" if empty_repo)] }
+.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
.row.gl-mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex
- .avatar-container.rect-avatar.s64.home-panel-avatar.gl-mr-3.float-none
+ .avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.gl-w-11.gl-h-11.gl-mr-3.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } }
+ %h1.home-panel-title.gl-mt-3.gl-mb-2.gl-font-size-h1.gl-line-height-24.gl-font-weight-bold{ data: { qa_selector: 'project_name_content' } }
= @project.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
- .home-panel-metadata.d-flex.flex-wrap.text-secondary
+ .home-panel-metadata.d-flex.flex-wrap.text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal
- if can?(current_user, :read_project, @project)
%span.text-secondary
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -23,8 +23,8 @@
%span.access-request-links.gl-ml-3
= render 'shared/members/access_request_links', source: @project
- if @project.tag_list.present?
- %span.home-panel-topic-list.mt-2.w-100.d-inline-flex
- = sprite_icon('tag', css_class: 'icon gl-mr-2')
+ %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal
+ = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- @project.topics_to_show.each do |topic|
- project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index 643d111fedd..30b0631b465 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -1,12 +1,15 @@
-.alert.alert-warning
- %h4
+.gl-alert.gl-alert-warning.gl-mb-5
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon')
+ %h4.gl-alert-title
= _("Too many changes to show.")
- .float-right
- - if current_controller?(:commit)
- = link_to _("Plain diff"), project_commit_path(@project, @commit, format: :diff), class: "btn btn-sm"
- = link_to _("Email patch"), project_commit_path(@project, @commit, format: :patch), class: "btn btn-sm"
- - elsif current_controller?('projects/merge_requests/diffs') && @merge_request&.persisted?
- = link_to _("Plain diff"), merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
- = link_to _("Email patch"), merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
- %p
+ .gl-alert-body
= html_escape(_("To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed.")) % { display_size: diff_files.size, real_size: diff_files.real_size, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
+ .gl-alert-actions
+ - if current_controller?(:commit)
+ = link_to _("Plain diff"), project_commit_path(@project, @commit, format: :diff), class: "btn gl-alert-action btn-default gl-button btn-default-secondary"
+ = link_to _("Email patch"), project_commit_path(@project, @commit, format: :patch), class: "btn gl-alert-action btn-default gl-button btn-default-secondary"
+ - elsif current_controller?('projects/merge_requests/diffs') && @merge_request&.persisted?
+ = link_to _("Plain diff"), merge_request_path(@merge_request, format: :diff), class: "btn gl-alert-action btn-default gl-button btn-default-secondary"
+ = link_to _("Email patch"), merge_request_path(@merge_request, format: :patch), class: "btn gl-alert-action btn-default gl-button btn-default-secondary"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d33023f66d6..1dbcd613ceb 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -8,6 +8,7 @@
- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
- number_of_pipelines = @pipelines.size
- mr_action = j(params[:tab].presence || 'show')
+- add_page_specific_style 'page_bundles/merge_requests'
- add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/reports'
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
index b4b06640bd9..a983a736a1e 100644
--- a/app/views/shared/members/_access_request_links.html.haml
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -5,13 +5,13 @@
= link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source), qa_selector: 'leave_group_link' },
- class: 'access-request-link js-leave-link'
+ class: '.gl-pl-3.gl-border-l-1.gl-border-l-solid.gl-border-l-gray-500 js-leave-link'
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(requester) },
- class: 'access-request-link'
+ class: '.gl-pl-3.gl-border-l-1.gl-border-l-solid.gl-border-l-gray-500'
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
method: :post,
- class: 'access-request-link'
+ class: '.gl-pl-3.gl-border-l-1.gl-border-l-solid.gl-border-l-gray-500'
diff --git a/changelogs/unreleased/233694-replace-bootstrap-alerts-in-app-views-projects-diffs-_warning-html.yml b/changelogs/unreleased/233694-replace-bootstrap-alerts-in-app-views-projects-diffs-_warning-html.yml
new file mode 100644
index 00000000000..ec4537484fe
--- /dev/null
+++ b/changelogs/unreleased/233694-replace-bootstrap-alerts-in-app-views-projects-diffs-_warning-html.yml
@@ -0,0 +1,5 @@
+---
+title: Replace bootstrap alerts in app/views/projects/diffs/_warning.html.haml
+merge_request: 41295
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/235108-migrate-terraform-states-to-versioniong.yml b/changelogs/unreleased/235108-migrate-terraform-states-to-versioniong.yml
new file mode 100644
index 00000000000..90f89077c0c
--- /dev/null
+++ b/changelogs/unreleased/235108-migrate-terraform-states-to-versioniong.yml
@@ -0,0 +1,5 @@
+---
+title: Seed initial version for non-versioned terraform states
+merge_request: 43665
+author:
+type: added
diff --git a/changelogs/unreleased/eread-migrate-tooltip-time-track-collapsed.yml b/changelogs/unreleased/eread-migrate-tooltip-time-track-collapsed.yml
new file mode 100644
index 00000000000..e519f650ef3
--- /dev/null
+++ b/changelogs/unreleased/eread-migrate-tooltip-time-track-collapsed.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate collapsed time tracking tooltip
+merge_request: 44874
+author:
+type: other
diff --git a/changelogs/unreleased/improve-gfm-ac-emoji.yml b/changelogs/unreleased/improve-gfm-ac-emoji.yml
new file mode 100644
index 00000000000..c8eaea58005
--- /dev/null
+++ b/changelogs/unreleased/improve-gfm-ac-emoji.yml
@@ -0,0 +1,5 @@
+---
+title: Match against description and unicode character when autocompleting GFM emoji
+merge_request: 42669
+author: Ethan Reesor (@firelizzard)
+type: added
diff --git a/config/application.rb b/config/application.rb
index ee4a05e9726..9c8a7d946b1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -182,6 +182,7 @@ module Gitlab
config.assets.precompile << "page_bundles/issues_list.css"
config.assets.precompile << "page_bundles/jira_connect.css"
config.assets.precompile << "page_bundles/merge_conflicts.css"
+ config.assets.precompile << "page_bundles/merge_requests.css"
config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/pipeline.css"
config.assets.precompile << "page_bundles/pipelines.css"
diff --git a/config/feature_flags/development/ci_manual_bridges.yml b/config/feature_flags/development/ci_manual_bridges.yml
new file mode 100644
index 00000000000..f654839e37c
--- /dev/null
+++ b/config/feature_flags/development/ci_manual_bridges.yml
@@ -0,0 +1,7 @@
+---
+name: ci_manual_bridges
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44011
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263412
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/initializers/sprockets.rb b/config/initializers/sprockets.rb
index a20b7dc75e9..bd33245d470 100644
--- a/config/initializers/sprockets.rb
+++ b/config/initializers/sprockets.rb
@@ -1 +1,3 @@
+require 'terser'
+
Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor
diff --git a/db/post_migrate/20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb b/db/post_migrate/20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb
new file mode 100644
index 00000000000..eff6ebfe5b4
--- /dev/null
+++ b/db/post_migrate/20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateInitialVersionsForPreVersioningTerraformStates < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ INSERT INTO terraform_state_versions (terraform_state_id, created_at, updated_at, version, file_store, file)
+ SELECT id, NOW(), NOW(), 0, file_store, file
+ FROM terraform_states
+ WHERE versioning_enabled = FALSE
+ ON CONFLICT (terraform_state_id, version) DO NOTHING
+ SQL
+ end
+
+ def down
+ end
+end
diff --git a/db/schema_migrations/20200929052138 b/db/schema_migrations/20200929052138
new file mode 100644
index 00000000000..05c56a31270
--- /dev/null
+++ b/db/schema_migrations/20200929052138
@@ -0,0 +1 @@
+30b84d137fcb17eaca86f1bec52d6e20c972f7083d4c983e2bb397c9126b5f0c \ No newline at end of file
diff --git a/doc/articles/artifactory_and_gitlab/index.md b/doc/articles/artifactory_and_gitlab/index.md
deleted file mode 100644
index ed9fd135e7c..00000000000
--- a/doc/articles/artifactory_and_gitlab/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../ci/examples/artifactory_and_gitlab/index.md'
----
-
-This document was moved to [another location](../../ci/examples/artifactory_and_gitlab/index.md)
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
deleted file mode 100644
index 2fbeb6f2506..00000000000
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../administration/auth/ldap/index.md'
----
-
-This document was moved to [another location](../../administration/auth/ldap/index.md).
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ee/index.md b/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
deleted file mode 100644
index 2fbeb6f2506..00000000000
--- a/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../administration/auth/ldap/index.md'
----
-
-This document was moved to [another location](../../administration/auth/ldap/index.md).
diff --git a/doc/articles/how_to_install_git/index.md b/doc/articles/how_to_install_git/index.md
deleted file mode 100644
index 62598101895..00000000000
--- a/doc/articles/how_to_install_git/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../topics/git/how_to_install_git/index.md'
----
-
-This document was moved to [another location](../../topics/git/how_to_install_git/index.md).
diff --git a/doc/articles/index.md b/doc/articles/index.md
deleted file mode 100644
index 4b965b0256f..00000000000
--- a/doc/articles/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../topics/index.md'
----
-
-This document was moved to [another location](../topics/index.md)
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/index.md b/doc/articles/laravel_with_gitlab_and_envoy/index.md
deleted file mode 100644
index fa4f6243410..00000000000
--- a/doc/articles/laravel_with_gitlab_and_envoy/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../ci/examples/laravel_with_gitlab_and_envoy/index.md'
----
-
-This document was moved to [another location](../../ci/examples/laravel_with_gitlab_and_envoy/index.md).
diff --git a/doc/articles/numerous_undo_possibilities_in_git/index.md b/doc/articles/numerous_undo_possibilities_in_git/index.md
deleted file mode 100644
index 83aac82db4e..00000000000
--- a/doc/articles/numerous_undo_possibilities_in_git/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../../topics/git/numerous_undo_possibilities_in_git/index.md'
----
-
-This document was moved to [another location](../../topics/git/numerous_undo_possibilities_in_git/index.md).
diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md
deleted file mode 100644
index 822d012aa3d..00000000000
--- a/doc/articles/openshift_and_gitlab/index.md
+++ /dev/null
@@ -1,3 +0,0 @@
----
-redirect_to: '../../install/openshift_and_gitlab/index.html'
----
diff --git a/doc/articles/runner_autoscale_aws/index.md b/doc/articles/runner_autoscale_aws/index.md
deleted file mode 100644
index fb769731256..00000000000
--- a/doc/articles/runner_autoscale_aws/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: 'https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/index.html'
----
-
-This document was moved to [another location](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/index.html).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 0ead0695acc..26674bb86eb 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2390,9 +2390,9 @@ To trigger a manual job, a user must have permission to merge to the assigned br
You can use [protected branches](../../user/project/protected_branches.md) to more strictly
[protect manual deployments](#protecting-manual-jobs) from being run by unauthorized users.
-`when:manual` and [`trigger`](#trigger) cannot be used together. If you use both in
-the same job, you receive a `jobs:#{job-name} when should be on_success, on_failure or always`
-error.
+In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
+can use `when:manual` in the same job as [`trigger`](#trigger). In GitLab 13.4 and
+earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
##### Protecting manual jobs **(PREMIUM)**
@@ -3643,10 +3643,9 @@ You can use this keyword to create two different types of downstream pipelines:
see which job triggered a downstream pipeline by hovering your mouse cursor over
the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines).
-NOTE: **Note:**
-Using a `trigger` with `when:manual` together results in the error `jobs:#{job-name}
-when should be on_success, on_failure or always`, because `when:manual` prevents
-triggers being used.
+In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you
+can use [`when:manual`](#whenmanual) in the same job as `trigger`. In GitLab 13.4 and
+earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`.
#### Simple `trigger` syntax for multi-project pipelines
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
deleted file mode 100644
index b98d1b51999..00000000000
--- a/doc/container_registry/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/packages/container_registry/index.md'
----
-
-This document was moved to [another location](../user/packages/container_registry/index.md).
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
deleted file mode 100644
index 092d7831e35..00000000000
--- a/doc/container_registry/troubleshooting.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/packages/container_registry/index.md#troubleshooting-the-gitlab-container-registry'
----
-
-This document was moved to [user/project/container_registry](../user/packages/container_registry/index.md#troubleshooting-the-gitlab-container-registry).
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index ed84eab48db..20ed0cbc39e 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -287,9 +287,8 @@ The table below shows what kind of documentation goes where.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic_name/subtopic_name/index.md` when subtopics become necessary.
General user- and admin- related documentation, should be placed accordingly.
-1. The directories `/workflow/`, `/university/`, and `/articles/` have been
- *deprecated* and the majority their documentation has been moved to their
- correct location in small iterations.
+1. The `/university/` directory is *deprecated* and the majority of its documentation
+ has been moved.
If you are unsure where to place a document or a content addition, this should
not stop you from authoring and contributing. You can use your best judgment and
diff --git a/doc/getting-started/subscription.md b/doc/getting-started/subscription.md
deleted file mode 100644
index 8bcd11c20c8..00000000000
--- a/doc/getting-started/subscription.md
+++ /dev/null
@@ -1,3 +0,0 @@
----
-redirect_to: '../subscriptions/index.md'
----
diff --git a/doc/git_hooks/git_hooks.md b/doc/git_hooks/git_hooks.md
deleted file mode 100644
index b251e58410a..00000000000
--- a/doc/git_hooks/git_hooks.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../push_rules/push_rules.md'
----
-
-This document was moved to [another location](../push_rules/push_rules.md)
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
deleted file mode 100644
index c6d44bb03e9..00000000000
--- a/doc/hooks/custom_hooks.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-redirect_to: '../administration/server_hooks.md'
----
-
-# Custom Git Hooks
-
-This document was moved to [administration/server_hooks.md](../administration/server_hooks.md).
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
deleted file mode 100644
index 9544983974f..00000000000
--- a/doc/incoming_email/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../administration/reply_by_email.md'
----
-
-This document was moved to [administration/reply_by_email](../administration/reply_by_email.md).
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
deleted file mode 100644
index a7192325229..00000000000
--- a/doc/incoming_email/postfix.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../administration/reply_by_email_postfix_setup.md'
----
-
-This document was moved to [administration/reply_by_email_postfix_setup](../administration/reply_by_email_postfix_setup.md).
diff --git a/doc/logs/logs.md b/doc/logs/logs.md
deleted file mode 100644
index 0cb092c85fd..00000000000
--- a/doc/logs/logs.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../administration/logs.md'
----
-
-This document was moved to [administration/logs.md](../administration/logs.md).
diff --git a/doc/pages/README.md b/doc/pages/README.md
deleted file mode 100644
index c67847f1a83..00000000000
--- a/doc/pages/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/project/pages/index.md'
----
-
-This document was moved to [another location](../user/project/pages/index.md).
diff --git a/doc/pages/administration.md b/doc/pages/administration.md
deleted file mode 100644
index 015dd54ec7f..00000000000
--- a/doc/pages/administration.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../administration/pages/index.md'
----
-
-This document was moved to [another location](../administration/pages/index.md).
diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md
deleted file mode 100644
index a0feed0b477..00000000000
--- a/doc/pages/getting_started_part_one.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/project/pages/getting_started_part_one.md'
----
-
-This document was moved to [another location](../user/project/pages/getting_started_part_one.md).
diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md
deleted file mode 100644
index 31a01a6c83b..00000000000
--- a/doc/pages/getting_started_part_three.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/project/pages/custom_domains_ssl_tls_certification/index.md'
----
-
-This document was moved to [another location](../user/project/pages/custom_domains_ssl_tls_certification/index.md).
diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md
deleted file mode 100644
index 05353c171fc..00000000000
--- a/doc/pages/getting_started_part_two.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/project/pages/getting_started_part_two.md'
----
-
-This document was moved to [another location](../user/project/pages/getting_started_part_two.md).
diff --git a/doc/profile/README.md b/doc/profile/README.md
deleted file mode 100644
index 4932cf33b87..00000000000
--- a/doc/profile/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/profile/index.md'
----
-
-This document was moved to [user/profile/account](../user/profile/index.md).
diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md
deleted file mode 100644
index cf99bd61f5d..00000000000
--- a/doc/profile/preferences.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/profile/preferences.md'
----
-
-This document was moved to [another location](../user/profile/preferences.md).
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
deleted file mode 100644
index 453ac833f59..00000000000
--- a/doc/profile/two_factor_authentication.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/profile/account/two_factor_authentication.md'
----
-
-This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md).
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index 09a70dda60d..bc8df63e33f 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -61,7 +61,7 @@ Access the default page for admin area settings by navigating to **Admin Area >
| ------ | ----------- |
| [Continuous Integration and Deployment](continuous_integration.md) | Auto DevOps, runners and job artifacts. |
| [Required pipeline configuration](continuous_integration.md#required-pipeline-configuration) **(PREMIUM ONLY)** | Set an instance-wide auto included [pipeline configuration](../../../ci/yaml/README.md). This pipeline configuration will be run after the project's own configuration. |
-| [Package Registry](continuous_integration.md#package-registry-configuration) | Settings related to the use and experience of using GitLab's Package Registry. Note there are [risks involved](./../../packages/container_registry/index.md#use-with-external-container-registries) in enabling some of these settings. |
+| [Package Registry](continuous_integration.md#package-registry-configuration) | Settings related to the use and experience of using GitLab's Package Registry. Note there are [risks involved](../../packages/container_registry/index.md#use-with-external-container-registries) in enabling some of these settings. |
## Reporting
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
deleted file mode 100644
index fffb6a5d86d..00000000000
--- a/doc/web_hooks/web_hooks.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-redirect_to: '../user/project/integrations/webhooks.md'
----
-
-This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md).
diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb
index a8b67a1db4f..1740032e5c7 100644
--- a/lib/gitlab/ci/config/entry/bridge.rb
+++ b/lib/gitlab/ci/config/entry/bridge.rb
@@ -11,15 +11,18 @@ module Gitlab
class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Ci::Config::Entry::Processable
+ ALLOWED_WHEN = %w[on_success on_failure always manual].freeze
ALLOWED_KEYS = %i[trigger].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS
with_options allow_nil: true do
- validates :when,
- inclusion: { in: %w[on_success on_failure always],
- message: 'should be on_success, on_failure or always' }
+ validates :allow_failure, boolean: true
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
end
validate on: :composed do
@@ -57,11 +60,19 @@ module Gitlab
true
end
+ def manual_action?
+ self.when == 'manual'
+ end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
+
def value
super.merge(
trigger: (trigger_value if trigger_defined?),
needs: (needs_value if needs_defined?),
- ignore: !!allow_failure,
+ ignore: ignored?,
when: self.when,
scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage
).compact
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 2b485167010..5bb6234bbb8 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -70,6 +70,10 @@ module Gitlab
def self.one_dimensional_matrix_enabled?
::Feature.enabled?(:one_dimensional_matrix, default_enabled: false)
end
+
+ def self.manual_bridges_enabled?(project)
+ ::Feature.enabled?(:ci_manual_bridges, project, default_enabled: false)
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/bridge/action.rb b/lib/gitlab/ci/status/bridge/action.rb
new file mode 100644
index 00000000000..1ba4700d9b0
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/action.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Action < Status::Build::Action
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/factory.rb b/lib/gitlab/ci/status/bridge/factory.rb
index 5d397dba0de..b9bd66cee71 100644
--- a/lib/gitlab/ci/status/bridge/factory.rb
+++ b/lib/gitlab/ci/status/bridge/factory.rb
@@ -6,7 +6,10 @@ module Gitlab
module Bridge
class Factory < Status::Factory
def self.extended_statuses
- [Status::Bridge::Failed]
+ [[Status::Bridge::Failed],
+ [Status::Bridge::Manual],
+ [Status::Bridge::Play],
+ [Status::Bridge::Action]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/bridge/manual.rb b/lib/gitlab/ci/status/bridge/manual.rb
new file mode 100644
index 00000000000..e07e645a34d
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/manual.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Manual < Status::Build::Manual
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/bridge/play.rb b/lib/gitlab/ci/status/bridge/play.rb
new file mode 100644
index 00000000000..ae00ef6c2ad
--- /dev/null
+++ b/lib/gitlab/ci/status/bridge/play.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Bridge
+ class Play < Status::Build::Play
+ def has_action?
+ can?(user, :play_job, subject)
+ end
+
+ def self.matches?(bridge, user)
+ bridge.playable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 008f679e583..4db6f3b8b55 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -746,6 +746,9 @@ msgid_plural "%{securityScanner} results are not available because a pipeline ha
msgstr[0] ""
msgstr[1] ""
+msgid "%{size} %{unit}"
+msgstr ""
+
msgid "%{size} GiB"
msgstr ""
@@ -28102,6 +28105,12 @@ msgstr ""
msgid "UsageQuota|LFS Storage"
msgstr ""
+msgid "UsageQuota|Learn more about excess storage usage"
+msgstr ""
+
+msgid "UsageQuota|Learn more about usage quotas"
+msgstr ""
+
msgid "UsageQuota|Packages"
msgstr ""
@@ -28111,6 +28120,9 @@ msgstr ""
msgid "UsageQuota|Purchase more storage"
msgstr ""
+msgid "UsageQuota|Purchased storage available"
+msgstr ""
+
msgid "UsageQuota|Repositories"
msgstr ""
@@ -28123,6 +28135,9 @@ msgstr ""
msgid "UsageQuota|Storage"
msgstr ""
+msgid "UsageQuota|This namespace contains locked projects"
+msgstr ""
+
msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr ""
@@ -28132,6 +28147,12 @@ msgstr ""
msgid "UsageQuota|This project is locked."
msgstr ""
+msgid "UsageQuota|Total excess storage used"
+msgstr ""
+
+msgid "UsageQuota|Total namespace storage used"
+msgstr ""
+
msgid "UsageQuota|Unlimited"
msgstr ""
@@ -30193,6 +30214,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "Your purchased storage is running low. To avoid locked projects, please purchase more storage."
+msgstr ""
+
msgid "Your request for access could not be processed: %{error_meesage}"
msgstr ""
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index 57ab7fb4480..95759d3b603 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -16,8 +16,9 @@ module QA
end
view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do
- element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern
+ element :job_item_container
element :job_link
+ element :action_button
end
view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
@@ -40,10 +41,12 @@ module QA
end
def has_build?(name, status: :success, wait: nil)
- within('.pipeline-graph') do
- within('.ci-job-component', text: name) do
+ if status
+ within_element(:job_item_container, text: name) do
has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
end
+ else
+ has_element?(:job_item_container, text: name)
end
end
@@ -78,6 +81,12 @@ module QA
def click_on_first_job
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
end
+
+ def click_job_action(job_name)
+ within_element(:job_item_container, text: job_name) do
+ click_element(:action_button)
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb
new file mode 100644
index 00000000000..39d5fbaba6b
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'faker'
+
+module QA
+ RSpec.describe 'Verify', :runner, :requires_admin do
+ # [TODO]: Developer to remove :requires_admin once FF is removed in follow up issue
+
+ describe "Trigger child pipeline with 'when:manual'" do
+ let(:feature_flag) { :ci_manual_bridges } # [TODO]: Developer to remove when feature flag is removed
+ let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-with-pipeline'
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.project = project
+ runner.name = executor
+ runner.tags = [executor]
+ end
+ end
+
+ before do
+ Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
+ Flow::Login.sign_in
+ add_ci_files
+ project.visit!
+ view_the_last_pipeline
+ end
+
+ after do
+ Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
+ runner.remove_via_api!
+ end
+
+ it 'can trigger bridge job', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1049' do
+ Page::Project::Pipeline::Show.perform do |parent_pipeline|
+ expect(parent_pipeline).not_to have_child_pipeline
+
+ parent_pipeline.click_job_action('trigger')
+ Support::Waiter.wait_until { parent_pipeline.has_child_pipeline? }
+ parent_pipeline.expand_child_pipeline
+
+ expect(parent_pipeline).to have_build('child_build', status: nil)
+ end
+ end
+
+ private
+
+ def add_ci_files
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add parent and child pipelines CI files.'
+ commit.add_files(
+ [
+ child_ci_file,
+ parent_ci_file
+ ]
+ )
+ end
+ end
+
+ def view_the_last_pipeline
+ Page::Project::Menu.perform(&:click_ci_cd_pipelines)
+ Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success)
+ Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
+ end
+
+ def parent_ci_file
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ build:
+ stage: build
+ tags: ["#{executor}"]
+ script: echo build
+
+ trigger:
+ stage: test
+ when: manual
+ trigger:
+ include: '.child-pipeline.yml'
+
+ deploy:
+ stage: deploy
+ tags: ["#{executor}"]
+ script: echo deploy
+ YAML
+ }
+ end
+
+ def child_ci_file
+ {
+ file_path: '.child-pipeline.yml',
+ content: <<~YAML
+ child_build:
+ stage: build
+ tags: ["#{executor}"]
+ script: echo build
+ YAML
+ }
+ end
+ end
+ end
+end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 11550c748ee..0ce989738df 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -40,8 +40,8 @@ then
fi
# Do not use 'README.md', instead use 'index.md'
-# Number of 'README.md's as of 2020-05-28
-NUMBER_READMES=40
+# Number of 'README.md's as of 2020-10-13
+NUMBER_READMES=36
FIND_READMES=$(find doc/ -name "README.md" | wc -l)
echo '=> Checking for new README.md files...'
echo
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index eff98ab65a6..80cb16966e5 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -757,19 +757,21 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
name: 'master', project: project)
sign_in(user)
-
- post_play
end
context 'when job is playable' do
let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
it 'redirects to the played job page' do
+ post_play
+
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end
it 'transits to pending' do
+ post_play
+
expect(job.reload).to be_pending
end
@@ -777,15 +779,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] }
it 'assigns the job variables' do
+ post_play
+
expect(job.reload.job_variables.map(&:key)).to contain_exactly('first')
end
end
+
+ context 'when job is bridge' do
+ let(:downstream_project) { create(:project) }
+ let(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
+
+ before do
+ downstream_project.add_developer(user)
+ end
+
+ it 'redirects to the pipeline page' do
+ post_play
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(pipeline_path(pipeline))
+ builds_namespace_project_pipeline_path(id: pipeline.id)
+ end
+
+ it 'transits to pending' do
+ post_play
+
+ expect(job.reload).to be_pending
+ end
+
+ context 'when FF ci_manual_bridges is disabled' do
+ before do
+ stub_feature_flags(ci_manual_bridges: false)
+ end
+
+ it 'returns 404' do
+ post_play
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
context 'when job is not playable' do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'renders unprocessable_entity' do
+ post_play
+
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
diff --git a/spec/controllers/projects/pipelines/stages_controller_spec.rb b/spec/controllers/projects/pipelines/stages_controller_spec.rb
index 6e8c08d95a1..a8b328c7563 100644
--- a/spec/controllers/projects/pipelines/stages_controller_spec.rb
+++ b/spec/controllers/projects/pipelines/stages_controller_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Pipelines::StagesController do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:downstream_project) { create(:project, :repository) }
before do
sign_in(user)
@@ -17,6 +18,7 @@ RSpec.describe Projects::Pipelines::StagesController do
before do
create_manual_build(pipeline, 'test', 'rspec 1/2')
create_manual_build(pipeline, 'test', 'rspec 2/2')
+ create_manual_bridge(pipeline, 'test', 'trigger')
pipeline.reload
end
@@ -32,6 +34,7 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when user has access' do
before do
project.add_maintainer(user)
+ downstream_project.add_maintainer(user)
end
context 'when the stage does not exists' do
@@ -46,12 +49,12 @@ RSpec.describe Projects::Pipelines::StagesController do
context 'when the stage exists' do
it 'starts all manual jobs' do
- expect(pipeline.builds.manual.count).to eq(2)
+ expect(pipeline.processables.manual.count).to eq(3)
play_manual_stage!
expect(response).to have_gitlab_http_status(:ok)
- expect(pipeline.builds.manual.count).to eq(0)
+ expect(pipeline.processables.manual.count).to eq(0)
end
end
end
@@ -68,5 +71,9 @@ RSpec.describe Projects::Pipelines::StagesController do
def create_manual_build(pipeline, stage, name)
create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name)
end
+
+ def create_manual_bridge(pipeline, stage, name)
+ create(:ci_bridge, :manual, pipeline: pipeline, stage: stage, name: name, downstream: downstream_project)
+ end
end
end
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index 5a33a30921b..7727a468633 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -40,6 +40,10 @@ FactoryBot.define do
end
end
+ trait :created do
+ status { 'created' }
+ end
+
trait :started do
started_at { '2013-10-29 09:51:28 CET' }
end
@@ -62,5 +66,14 @@ FactoryBot.define do
trait :strategy_depend do
options { { trigger: { strategy: 'depend' } } }
end
+
+ trait :manual do
+ status { 'manual' }
+ self.when { 'manual' }
+ end
+
+ trait :playable do
+ manual
+ end
end
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 865ae3ad8cb..e387ea4d473 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe "Compare", :js do
click_button('Compare')
- page.within('.alert') do
+ page.within('.gl-alert') do
expect(page).to have_text("Too many changes to show. To preserve performance only 3 of 3+ files are displayed.")
end
end
diff --git a/spec/frontend/emoji/emoji_spec.js b/spec/frontend/emoji/emoji_spec.js
index 2f174c45ad7..35cf3a62fff 100644
--- a/spec/frontend/emoji/emoji_spec.js
+++ b/spec/frontend/emoji/emoji_spec.js
@@ -1,7 +1,6 @@
-import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
-import axios from '~/lib/utils/axios_utils';
-import { initEmojiMap, glEmojiTag, searchEmoji, EMOJI_VERSION } from '~/emoji';
+import { emojiFixtureMap, initEmojiMock, describeEmojiFields } from 'helpers/emoji';
+import { glEmojiTag, searchEmoji } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
isRainbowFlagEmoji,
@@ -30,54 +29,11 @@ const emptySupportMap = {
1.1: false,
};
-const emojiFixtureMap = {
- atom: {
- name: 'atom',
- moji: '⚛',
- description: 'atom symbol',
- unicodeVersion: '4.1',
- },
- bomb: {
- name: 'bomb',
- moji: '💣',
- unicodeVersion: '6.0',
- description: 'bomb',
- },
- construction_worker_tone5: {
- name: 'construction_worker_tone5',
- moji: '👷🏿',
- unicodeVersion: '8.0',
- description: 'construction worker tone 5',
- },
- five: {
- name: 'five',
- moji: '5️⃣',
- unicodeVersion: '3.0',
- description: 'keycap digit five',
- },
- grey_question: {
- name: 'grey_question',
- moji: '❔',
- unicodeVersion: '6.0',
- description: 'white question mark ornament',
- },
-};
-
describe('gl_emoji', () => {
let mock;
- beforeEach(() => {
- const emojiData = Object.fromEntries(
- Object.values(emojiFixtureMap).map(m => {
- const { name: n, moji: e, unicodeVersion: u, category: c, description: d } = m;
- return [n, { c, e, d, u }];
- }),
- );
-
- mock = new MockAdapter(axios);
- mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(emojiData));
-
- return initEmojiMap().catch(() => {});
+ beforeEach(async () => {
+ mock = await initEmojiMock();
});
afterEach(() => {
@@ -398,21 +354,101 @@ describe('gl_emoji', () => {
describe('searchEmoji', () => {
const { atom, grey_question } = emojiFixtureMap;
- const contains = (e, term) =>
- expect(searchEmoji(term).map(({ name }) => name)).toContain(e.name);
+ const search = (query, opts) => searchEmoji(query, opts).map(({ name }) => name);
+ const mangle = str => str.slice(0, 1) + str.slice(-1);
+ const partial = str => str.slice(0, 2);
+
+ describe('with default options', () => {
+ const subject = query => search(query);
+
+ describeEmojiFields('with $field', ({ accessor }) => {
+ it(`should match by lower case: ${accessor(atom)}`, () => {
+ expect(subject(accessor(atom))).toContain(atom.name);
+ });
+
+ it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
+ expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
+ });
+
+ it(`should not match by partial: ${mangle(accessor(atom))}`, () => {
+ expect(subject(mangle(accessor(atom)))).not.toContain(atom.name);
+ });
+ });
+
+ it(`should match by unicode value: ${atom.moji}`, () => {
+ expect(subject(atom.moji)).toContain(atom.name);
+ });
+
+ it('should not return a fallback value', () => {
+ expect(subject('foo bar baz')).toHaveLength(0);
+ });
+ });
+
+ describe('with fuzzy match', () => {
+ const subject = query => search(query, { match: 'fuzzy' });
+
+ describeEmojiFields('with $field', ({ accessor }) => {
+ it(`should match by lower case: ${accessor(atom)}`, () => {
+ expect(subject(accessor(atom))).toContain(atom.name);
+ });
+
+ it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
+ expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
+ });
+
+ it(`should match by partial: ${mangle(accessor(atom))}`, () => {
+ expect(subject(mangle(accessor(atom)))).toContain(atom.name);
+ });
+ });
+ });
+
+ describe('with contains match', () => {
+ const subject = query => search(query, { match: 'contains' });
+
+ describeEmojiFields('with $field', ({ accessor }) => {
+ it(`should match by lower case: ${accessor(atom)}`, () => {
+ expect(subject(accessor(atom))).toContain(atom.name);
+ });
+
+ it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
+ expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
+ });
+
+ it(`should match by partial: ${partial(accessor(atom))}`, () => {
+ expect(subject(partial(accessor(atom)))).toContain(atom.name);
+ });
+
+ it(`should not match by mangled: ${mangle(accessor(atom))}`, () => {
+ expect(subject(mangle(accessor(atom)))).not.toContain(atom.name);
+ });
+ });
+ });
- it('should match by full name', () => contains(grey_question, 'grey_question'));
- it('should match by full alias', () => contains(atom, 'atom_symbol'));
- it('should match by full description', () => contains(grey_question, 'ornament'));
+ describe('with fallback', () => {
+ const subject = query => search(query, { fallback: true });
- it('should match by partial name', () => contains(grey_question, 'question'));
- it('should match by partial alias', () => contains(atom, '_symbol'));
- it('should match by partial description', () => contains(grey_question, 'ment'));
+ it('should return a fallback value', () =>
+ expect(subject('foo bar baz')).toContain(grey_question.name));
+ });
+
+ describe('with name and alias fields', () => {
+ const subject = query => search(query, { fields: ['name', 'alias'] });
- it('should fuzzy match by name', () => contains(grey_question, 'greion'));
- it('should fuzzy match by alias', () => contains(atom, 'atobol'));
- it('should fuzzy match by description', () => contains(grey_question, 'ornt'));
+ it(`should match by name: ${atom.name}`, () => {
+ expect(subject(atom.name)).toContain(atom.name);
+ });
+
+ it(`should match by alias: ${atom.aliases[0]}`, () => {
+ expect(subject(atom.aliases[0])).toContain(atom.name);
+ });
- it('should match by character', () => contains(grey_question, '❔'));
+ it(`should not match by description: ${atom.description}`, () => {
+ expect(subject(atom.description)).not.toContain(atom.name);
+ });
+
+ it(`should not match by unicode value: ${atom.moji}`, () => {
+ expect(subject(atom.moji)).not.toContain(atom.name);
+ });
+ });
});
});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 25cd606a094..ce59574ef61 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,6 +1,7 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
+import { emojiFixtureMap, initEmojiMock, describeEmojiFields } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
@@ -702,4 +703,62 @@ describe('GfmAutoComplete', () => {
`('$input shows $output.length labels', expectLabels);
});
});
+
+ describe('emoji', () => {
+ const { atom } = emojiFixtureMap;
+ const assertInserted = ({ input, subject, emoji }) =>
+ expect(subject).toBe(`:${emoji?.name || input}:`);
+ const assertTemplated = ({ input, subject, emoji }) =>
+ expect(subject.replace(/\s+/g, ' ')).toBe(
+ `<li>${input} <gl-emoji data-name="${emoji?.name || input}"></gl-emoji> </li>`,
+ );
+
+ let mock;
+
+ beforeEach(async () => {
+ mock = await initEmojiMock();
+
+ await new GfmAutoComplete({}).loadEmojiData({ atwho() {}, trigger() {} }, ':');
+ if (!GfmAutoComplete.glEmojiTag) throw new Error('emoji not loaded');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe.each`
+ name | inputFormat | assert
+ ${'insertTemplateFunction'} | ${name => ({ name })} | ${assertInserted}
+ ${'templateFunction'} | ${name => name} | ${assertTemplated}
+ `('Emoji.$name', ({ name, inputFormat, assert }) => {
+ const execute = (input, emoji) =>
+ assert({
+ input,
+ emoji,
+ subject: GfmAutoComplete.Emoji[name](inputFormat(input)),
+ });
+
+ describeEmojiFields('for $field', ({ accessor }) => {
+ it('should work with lowercase', () => {
+ execute(accessor(atom), atom);
+ });
+
+ it('should work with uppercase', () => {
+ execute(accessor(atom).toUpperCase(), atom);
+ });
+
+ it('should work with partial value', () => {
+ execute(accessor(atom).slice(1), atom);
+ });
+ });
+
+ it('should work with unicode value', () => {
+ execute(atom.moji, atom);
+ });
+
+ it('should pass through unknown value', () => {
+ execute('foo bar baz');
+ });
+ });
+ });
});
diff --git a/spec/frontend/helpers/emoji.js b/spec/frontend/helpers/emoji.js
new file mode 100644
index 00000000000..0f257c32a68
--- /dev/null
+++ b/spec/frontend/helpers/emoji.js
@@ -0,0 +1,66 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
+
+export const emojiFixtureMap = {
+ atom: {
+ name: 'atom',
+ moji: '⚛',
+ description: 'atom symbol',
+ unicodeVersion: '4.1',
+ aliases: ['atom_symbol'],
+ },
+ bomb: {
+ name: 'bomb',
+ moji: '💣',
+ unicodeVersion: '6.0',
+ description: 'bomb',
+ aliases: [],
+ },
+ construction_worker_tone5: {
+ name: 'construction_worker_tone5',
+ moji: '👷🏿',
+ unicodeVersion: '8.0',
+ description: 'construction worker tone 5',
+ aliases: [],
+ },
+ five: {
+ name: 'five',
+ moji: '5️⃣',
+ unicodeVersion: '3.0',
+ description: 'keycap digit five',
+ aliases: [],
+ },
+ grey_question: {
+ name: 'grey_question',
+ moji: '❔',
+ unicodeVersion: '6.0',
+ description: 'white question mark ornament',
+ aliases: [],
+ },
+};
+
+export async function initEmojiMock() {
+ const emojiData = Object.fromEntries(
+ Object.values(emojiFixtureMap).map(m => {
+ const { name: n, moji: e, unicodeVersion: u, category: c, description: d } = m;
+ return [n, { c, e, d, u }];
+ }),
+ );
+
+ const mock = new MockAdapter(axios);
+ mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(emojiData));
+
+ await initEmojiMap();
+
+ return mock;
+}
+
+export function describeEmojiFields(label, tests) {
+ describe.each`
+ field | accessor
+ ${'name'} | ${e => e.name}
+ ${'alias'} | ${e => e.aliases[0]}
+ ${'description'} | ${e => e.description}
+ `(label, tests);
+}
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index 2f8f1092612..f600f2bcd55 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -1,5 +1,6 @@
import {
formatRelevantDigits,
+ bytesToKB,
bytesToKiB,
bytesToMiB,
bytesToGiB,
@@ -54,6 +55,16 @@ describe('Number Utils', () => {
});
});
+ describe('bytesToKB', () => {
+ it.each`
+ input | output
+ ${1000} | ${1}
+ ${1024} | ${1.024}
+ `('returns $output KB for $input bytes', ({ input, output }) => {
+ expect(bytesToKB(input)).toBe(output);
+ });
+ });
+
describe('bytesToKiB', () => {
it('calculates KiB for the given bytes', () => {
expect(bytesToKiB(1024)).toEqual(1);
diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb
index 2ea215450cb..e73665a1b1d 100644
--- a/spec/graphql/types/snippet_type_spec.rb
+++ b/spec/graphql/types/snippet_type_spec.rb
@@ -124,7 +124,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
end
describe '#blob' do
- let(:query_blob) { subject.dig('data', 'snippets', 'edges')[0]['node']['blob'] }
+ let(:query_blob) { subject.dig('data', 'snippets', 'nodes')[0]['blob'] }
subject { GitlabSchema.execute(snippet_query_for(field: 'blob'), context: { current_user: user }).as_json }
@@ -151,21 +151,17 @@ RSpec.describe GitlabSchema.types['Snippet'] do
describe '#blobs' do
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
- let(:query_blobs) { subject.dig('data', 'snippets', 'edges')[0].dig('node', 'blobs', 'edges') }
+ let(:query_blobs) { subject.dig('data', 'snippets', 'nodes')[0].dig('blobs', 'nodes') }
let(:paths) { [] }
let(:query) do
%(
{
snippets {
- edges {
- node {
- blobs(paths: #{paths}) {
- edges {
- node {
- name
- path
- }
- }
+ nodes {
+ blobs(paths: #{paths}) {
+ nodes {
+ name
+ path
}
}
}
@@ -188,8 +184,8 @@ RSpec.describe GitlabSchema.types['Snippet'] do
it_behaves_like 'an array'
it 'contains the first blob from the snippet' do
- expect(query_blobs.first['node']['name']).to eq blob.name
- expect(query_blobs.first['node']['path']).to eq blob.path
+ expect(query_blobs.first['name']).to eq blob.name
+ expect(query_blobs.first['path']).to eq blob.path
end
end
@@ -200,7 +196,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
it_behaves_like 'an array'
it 'contains all the blobs from the repository' do
- resulting_blobs_names = query_blobs.map { |b| b['node']['name'] }
+ resulting_blobs_names = query_blobs.map { |b| b['name'] }
expect(resulting_blobs_names).to match_array(blobs.map(&:name))
end
@@ -211,7 +207,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
it_behaves_like 'an array'
it 'returns specific files' do
- resulting_blobs_names = query_blobs.map { |b| b['node']['name'] }
+ resulting_blobs_names = query_blobs.map { |b| b['name'] }
expect(resulting_blobs_names).to match(paths)
end
@@ -223,12 +219,10 @@ RSpec.describe GitlabSchema.types['Snippet'] do
%(
{
snippets {
- edges {
- node {
- #{field} {
- name
- path
- }
+ nodes {
+ #{field} {
+ name
+ path
}
}
}
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index f33176c3da3..8b2e0410474 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -228,4 +228,66 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ describe '#manual_action?' do
+ context 'when job is a manual action' do
+ let(:config) { { script: 'deploy', when: 'manual' } }
+
+ it { is_expected.to be_manual_action }
+ end
+
+ context 'when job is not a manual action' do
+ let(:config) { { script: 'deploy' } }
+
+ it { is_expected.not_to be_manual_action }
+ end
+ end
+
+ describe '#ignored?' do
+ context 'when job is a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual' }
+ end
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: true }
+ end
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: false }
+ end
+
+ it { is_expected.not_to be_ignored }
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) { { script: 'deploy' } }
+
+ it { is_expected.not_to be_ignored }
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: true } }
+
+ it { is_expected.to be_ignored }
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: false } }
+
+ it { is_expected.not_to be_ignored }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index 021b777a0ff..d27bb98ba9a 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is created' do
- let(:bridge) { create(:ci_bridge) }
+ let(:bridge) { create_bridge(:created) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Created
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
context 'when bridge is failed' do
- let(:bridge) { create(:ci_bridge, :failed) }
+ let(:bridge) { create_bridge(:failed) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
@@ -70,4 +70,61 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
end
end
+
+ context 'when bridge is a manual action' do
+ let(:bridge) { create_bridge(:playable) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Bridge::Manual,
+ Gitlab::Ci::Status::Bridge::Play,
+ Gitlab::Ci::Status::Bridge::Action]
+ end
+
+ it 'fabricates action detailed status' do
+ expect(status).to be_a Gitlab::Ci::Status::Bridge::Action
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq s_('CiStatusText|manual')
+ expect(status.group).to eq 'manual'
+ expect(status.icon).to eq 'status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
+ expect(status.illustration).to include(:image, :size, :title, :content)
+ expect(status.label).to include 'manual play action'
+ expect(status).not_to have_details
+ expect(status.action_path).to include 'play'
+ end
+
+ context 'when user has ability to play action' do
+ before do
+ bridge.downstream_project.add_developer(user)
+ end
+
+ it 'fabricates status that has action' do
+ expect(status).to have_action
+ end
+ end
+
+ context 'when user does not have ability to play action' do
+ it 'fabricates status that has no action' do
+ expect(status).not_to have_action
+ end
+ end
+ end
+
+ private
+
+ def create_bridge(trait)
+ upstream_project = create(:project, :repository)
+ downstream_project = create(:project, :repository)
+ upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project)
+ trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } }
+
+ create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline)
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 2df15c8f400..edb9a613f85 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -413,6 +413,7 @@ project:
- stages
- ci_refs
- builds
+- processables
- runner_projects
- runners
- variables
diff --git a/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb b/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
new file mode 100644
index 00000000000..1a618712b32
--- /dev/null
+++ b/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb')
+
+RSpec.describe CreateInitialVersionsForPreVersioningTerraformStates do
+ let(:namespace) { table(:namespaces).create!(name: 'terraform', path: 'terraform') }
+ let(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
+ let(:terraform_state_versions) { table(:terraform_state_versions) }
+
+ def create_state!(project, versioning_enabled:)
+ table(:terraform_states).create!(
+ project_id: project.id,
+ uuid: 'uuid',
+ file_store: 2,
+ file: 'state.tfstate',
+ versioning_enabled: versioning_enabled
+ )
+ end
+
+ describe '#up' do
+ context 'for a state that is already versioned' do
+ let!(:terraform_state) { create_state!(project, versioning_enabled: true) }
+
+ it 'does not insert a version record' do
+ expect { migrate! }.not_to change { terraform_state_versions.count }
+ end
+ end
+
+ context 'for a state that is not yet versioned' do
+ let!(:terraform_state) { create_state!(project, versioning_enabled: false) }
+
+ it 'creates a version using the current state data' do
+ expect { migrate! }.to change { terraform_state_versions.count }.by(1)
+
+ migrated_version = terraform_state_versions.last
+ expect(migrated_version.terraform_state_id).to eq(terraform_state.id)
+ expect(migrated_version.version).to be_zero
+ expect(migrated_version.file_store).to eq(terraform_state.file_store)
+ expect(migrated_version.file).to eq(terraform_state.file)
+ expect(migrated_version.created_at).to be_present
+ expect(migrated_version.updated_at).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 850fc1ec6e6..c464e176c17 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -59,30 +59,20 @@ RSpec.describe Ci::Bridge do
describe 'state machine transitions' do
context 'when bridge points towards downstream' do
- it 'schedules downstream pipeline creation' do
- expect(bridge).to receive(:schedule_downstream_pipeline!)
+ %i[created manual].each do |status|
+ it "schedules downstream pipeline creation when the status is #{status}" do
+ bridge.status = status
- bridge.enqueue!
- end
- end
- end
-
- describe 'state machine transitions' do
- context 'when bridge points towards downstream' do
- it 'schedules downstream pipeline creation' do
- expect(bridge).to receive(:schedule_downstream_pipeline!)
+ expect(bridge).to receive(:schedule_downstream_pipeline!)
- bridge.enqueue!
+ bridge.enqueue!
+ end
end
- end
- end
- describe 'state machine transitions' do
- context 'when bridge points towards downstream' do
- it 'schedules downstream pipeline creation' do
- expect(bridge).to receive(:schedule_downstream_pipeline!)
+ it 'raises error when the status is failed' do
+ bridge.status = :failed
- bridge.enqueue!
+ expect { bridge.enqueue! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
@@ -304,4 +294,67 @@ RSpec.describe Ci::Bridge do
end
end
end
+
+ describe '#play' do
+ let(:downstream_project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
+
+ subject { bridge.play(user) }
+
+ before do
+ project.add_maintainer(user)
+ downstream_project.add_maintainer(user)
+ end
+
+ it 'enqueues the bridge' do
+ subject
+
+ expect(bridge).to be_pending
+ end
+ end
+
+ describe '#playable?' do
+ context 'when bridge is a manual action' do
+ subject { build_stubbed(:ci_bridge, :manual).playable? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when FF ci_manual_bridges is disabled' do
+ before do
+ stub_feature_flags(ci_manual_bridges: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when build is not a manual action' do
+ subject { build_stubbed(:ci_bridge, :created).playable? }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#action?' do
+ context 'when bridge is a manual action' do
+ subject { build_stubbed(:ci_bridge, :manual).action? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when FF ci_manual_bridges is disabled' do
+ before do
+ stub_feature_flags(ci_manual_bridges: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when build is not a manual action' do
+ subject { build_stubbed(:ci_bridge, :created).action? }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index 1d99d103bb8..608c5bdf03a 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -87,11 +87,17 @@ RSpec.describe Terraform::State do
let(:terraform_state) { create(:terraform_state, :with_file) }
it { is_expected.to eq terraform_state.file }
+
+ context 'and a version exists (migration to versioned in progress)' do
+ let!(:migrated_version) { create(:terraform_state_version, terraform_state: terraform_state) }
+
+ it { is_expected.to eq terraform_state.latest_version.file }
+ end
end
end
describe '#update_file!' do
- let(:version) { 2 }
+ let(:version) { 3 }
let(:data) { Hash[terraform_version: '0.12.21'].to_json }
subject { terraform_state.update_file!(CarrierWaveStringFile.new(data), version: version) }
@@ -115,6 +121,33 @@ RSpec.describe Terraform::State do
expect(terraform_state.latest_file.read).to eq(data)
end
+
+ context 'and a version exists (migration to versioned in progress)' do
+ let!(:migrated_version) { create(:terraform_state_version, terraform_state: terraform_state, version: 0) }
+
+ it 'creates a new version, corrects the migrated version number, and marks the state as versioned' do
+ expect { subject }.to change { Terraform::StateVersion.count }
+
+ expect(migrated_version.reload.version).to eq(1)
+ expect(migrated_version.file.read).to eq(terraform_state_file)
+
+ expect(terraform_state.reload.latest_version.version).to eq(version)
+ expect(terraform_state.latest_version.file.read).to eq(data)
+ expect(terraform_state).to be_versioning_enabled
+ end
+
+ context 'the current version cannot be determined' do
+ before do
+ migrated_version.update!(file: CarrierWaveStringFile.new('invalid-json'))
+ end
+
+ it 'uses version - 1 to correct the migrated version number' do
+ expect { subject }.to change { Terraform::StateVersion.count }
+
+ expect(migrated_version.reload.version).to eq(2)
+ end
+ end
+ end
end
end
end
diff --git a/spec/policies/ci/bridge_policy_spec.rb b/spec/policies/ci/bridge_policy_spec.rb
new file mode 100644
index 00000000000..e598e2f7626
--- /dev/null
+++ b/spec/policies/ci/bridge_policy_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BridgePolicy do
+ let_it_be(:user, reload: true) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:downstream_project, reload: true) { create(:project, :repository) }
+ let_it_be(:pipeline, reload: true) { create(:ci_empty_pipeline, project: project) }
+ let_it_be(:bridge, reload: true) { create(:ci_bridge, pipeline: pipeline, downstream: downstream_project) }
+
+ let(:policy) do
+ described_class.new(user, bridge)
+ end
+
+ describe '#play_job' do
+ before do
+ fake_access = double('Gitlab::UserAccess')
+ expect(fake_access).to receive(:can_update_branch?).with('master').and_return(can_update_branch)
+ expect(Gitlab::UserAccess).to receive(:new).with(user, container: downstream_project).and_return(fake_access)
+ end
+
+ context 'when user can update the downstream branch' do
+ let(:can_update_branch) { true }
+
+ it 'allows' do
+ expect(policy).to be_allowed :play_job
+ end
+ end
+
+ context 'when user can not update the downstream branch' do
+ let(:can_update_branch) { false }
+
+ it 'does not allow' do
+ expect(policy).not_to be_allowed :play_job
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/play_bridge_service_spec.rb b/spec/services/ci/play_bridge_service_spec.rb
new file mode 100644
index 00000000000..0482ad4d76f
--- /dev/null
+++ b/spec/services/ci/play_bridge_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PlayBridgeService, '#execute' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:downstream_project) { create(:project) }
+ let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
+ let(:instance) { described_class.new(project, user) }
+
+ subject(:execute_service) { instance.execute(bridge) }
+
+ context 'when user can run the bridge' do
+ before do
+ allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(true)
+ end
+
+ it 'marks the bridge pending' do
+ execute_service
+
+ expect(bridge.reload).to be_pending
+ end
+
+ it 'enqueues Ci::CreateCrossProjectPipelineWorker' do
+ expect(::Ci::CreateCrossProjectPipelineWorker).to receive(:perform_async).with(bridge.id)
+
+ execute_service
+ end
+
+ it "updates bridge's user" do
+ execute_service
+
+ expect(bridge.reload.user).to eq(user)
+ end
+
+ context 'when bridge is not playable' do
+ let(:bridge) { create(:ci_bridge, :failed, pipeline: pipeline, downstream: downstream_project) }
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect { execute_service }.to raise_error StateMachines::InvalidTransition
+ end
+ end
+ end
+
+ context 'when user can not run the bridge' do
+ before do
+ allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(false)
+ end
+
+ it 'allows user with developer role to play a bridge' do
+ expect { execute_service }.to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+end
diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb
index e30ec8bfda5..3e2a95ee975 100644
--- a/spec/services/ci/play_manual_stage_service_spec.rb
+++ b/spec/services/ci/play_manual_stage_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
let(:current_user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, user: current_user) }
let(:project) { pipeline.project }
+ let(:downstream_project) { create(:project) }
let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
let(:stage_status) { 'manual' }
@@ -18,40 +19,42 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
before do
project.add_maintainer(current_user)
+ downstream_project.add_maintainer(current_user)
create_builds_for_stage(status: stage_status)
+ create_bridge_for_stage(status: stage_status)
end
- context 'when pipeline has manual builds' do
+ context 'when pipeline has manual processables' do
before do
service.execute(stage)
end
- it 'starts manual builds from pipeline' do
- expect(pipeline.builds.manual.count).to eq(0)
+ it 'starts manual processables from pipeline' do
+ expect(pipeline.processables.manual.count).to eq(0)
end
- it 'updates manual builds' do
- pipeline.builds.each do |build|
- expect(build.user).to eq(current_user)
+ it 'updates manual processables' do
+ pipeline.processables.each do |processable|
+ expect(processable.user).to eq(current_user)
end
end
end
- context 'when pipeline has no manual builds' do
+ context 'when pipeline has no manual processables' do
let(:stage_status) { 'failed' }
before do
service.execute(stage)
end
- it 'does not update the builds' do
- expect(pipeline.builds.failed.count).to eq(3)
+ it 'does not update the processables' do
+ expect(pipeline.processables.failed.count).to eq(4)
end
end
- context 'when user does not have permission on a specific build' do
+ context 'when user does not have permission on a specific processable' do
before do
- allow_next_instance_of(Ci::Build) do |instance|
+ allow_next_instance_of(Ci::Processable) do |instance|
allow(instance).to receive(:play).and_raise(Gitlab::Access::AccessDeniedError)
end
@@ -60,12 +63,14 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
it 'logs the error' do
expect(Gitlab::AppLogger).to receive(:error)
- .exactly(stage.builds.manual.count)
+ .exactly(stage.processables.manual.count)
service.execute(stage)
end
end
+ private
+
def create_builds_for_stage(options)
options.merge!({
when: 'manual',
@@ -77,4 +82,17 @@ RSpec.describe Ci::PlayManualStageService, '#execute' do
create_list(:ci_build, 3, options)
end
+
+ def create_bridge_for_stage(options)
+ options.merge!({
+ when: 'manual',
+ pipeline: pipeline,
+ stage: stage.name,
+ stage_id: stage.id,
+ user: pipeline.user,
+ downstream: downstream_project
+ })
+
+ create(:ci_bridge, options)
+ end
end
diff --git a/spec/uploaders/terraform/versioned_state_uploader_spec.rb b/spec/uploaders/terraform/versioned_state_uploader_spec.rb
index ecc3f943480..eeb54cb61c7 100644
--- a/spec/uploaders/terraform/versioned_state_uploader_spec.rb
+++ b/spec/uploaders/terraform/versioned_state_uploader_spec.rb
@@ -12,9 +12,18 @@ RSpec.describe Terraform::VersionedStateUploader do
end
describe '#filename' do
- it 'contains the UUID of the terraform state record' do
+ it 'contains the version of the terraform state record' do
expect(subject.filename).to eq("#{model.version}.tfstate")
end
+
+ context 'legacy state with versioning disabled' do
+ let(:state) { create(:legacy_terraform_state) }
+ let(:model) { create(:terraform_state_version, terraform_state: state) }
+
+ it 'contains the UUID of the terraform state record' do
+ expect(subject.filename).to eq("#{model.uuid}.tfstate")
+ end
+ end
end
describe '#store_dir' do
@@ -25,5 +34,14 @@ RSpec.describe Terraform::VersionedStateUploader do
expect(subject.store_dir).to eq(:store_dir)
end
+
+ context 'legacy state with versioning disabled' do
+ let(:state) { create(:legacy_terraform_state) }
+ let(:model) { create(:terraform_state_version, terraform_state: state) }
+
+ it 'contains the ID of the project' do
+ expect(subject.store_dir).to include(model.project_id.to_s)
+ end
+ end
end
end