diff options
124 files changed, 2101 insertions, 1213 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0cc95ff8436..bd29a266ccd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -684,7 +684,6 @@ gitlab:assets:compile: - gitlab-org gitlab:ui:visual: - <<: *except-docs tags: - gitlab-org before_script: [] @@ -703,6 +702,12 @@ gitlab:ui:visual: - app/assets/stylesheets/*.scss - app/assets/stylesheets/**/*.scss - app/assets/stylesheets/**/**/*.scss + except: + refs: + - /(^docs[\/-].*|.*-docs$)/ + - master + variables: + - $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i artifacts: paths: - tests/__image_snapshots__/ diff --git a/.stylelintrc b/.stylelintrc index 04784a0a11a..c0f21aed292 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -102,6 +102,6 @@ "selector-pseudo-element-no-unknown":true, "shorthand-property-no-redundant-values":true, "string-quotes":"single", - "value-no-vendor-prefix":true + "value-no-vendor-prefix":[true, { ignoreValues: ["sticky"] }] } } @@ -267,7 +267,6 @@ gem 'gemojione', '~> 3.3' gem 'gon', '~> 6.2' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'request_store', '~> 1.3' -gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'base32', '~> 0.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 4d37075cdfa..634029e03de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -811,8 +811,6 @@ GEM seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) - select2-rails (3.5.9.3) - thor (~> 0.14) selenium-webdriver (3.12.0) childprocess (~> 0.5) rubyzip (~> 1.2) @@ -1144,7 +1142,6 @@ DEPENDENCIES sass-rails (~> 5.0.6) scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) - select2-rails (~> 3.5.9) selenium-webdriver (~> 3.12) sentry-raven (~> 2.7) settingslogic (~> 2.0.9) diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index f74cd71de04..13e8617c515 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -89,53 +89,26 @@ export default { ingressInstalled() { return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED; }, - ingressExternalIp() { - return this.applications.ingress.externalIp; + ingressExternalEndpoint() { + return this.applications.ingress.externalIp || this.applications.ingress.externalHostname; }, certManagerInstalled() { return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED; }, ingressDescription() { - const extraCostParagraph = sprintf( - _.escape( - s__( - `ClusterIntegration|%{boldNotice} This will add some extra resources - like a load balancer, which may incur additional costs depending on - the hosting provider your Kubernetes cluster is installed on. If you are using - Google Kubernetes Engine, you can %{pricingLink}.`, - ), - ), - { - boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, - pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, - }, - false, - ); - - const externalIpParagraph = sprintf( + return sprintf( _.escape( s__( - `ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS - at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`, + `ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}.`, ), ), { - ingressHelpLink: `<a href="${this.ingressHelpPath}"> - ${_.escape(s__('ClusterIntegration|More information'))} - </a>`, + pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb" + target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`, }, false, ); - - return ` - <p> - ${extraCostParagraph} - </p> - <p class="settings-message append-bottom-0"> - ${externalIpParagraph} - </p> - `; }, certManagerDescription() { return sprintf( @@ -196,11 +169,26 @@ export default { knativeUpgradeFailed() { return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED; }, - knativeExternalIp() { - return this.knative.externalIp; + knativeExternalEndpoint() { + return this.knative.externalIp || this.knative.externalHostname; + }, + knativeDescription() { + return sprintf( + _.escape( + s__( + `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`, + ), + ), + { + pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb" + target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`, + }, + false, + ); }, canUpdateKnativeEndpoint() { - return this.knativeExternalIp && !this.knativeUpgradeFailed && !this.knativeUpgrading; + return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading; }, knativeHostname: { get() { @@ -289,31 +277,31 @@ export default { <template v-if="ingressInstalled"> <div class="form-group"> - <label for="ingress-ip-address"> - {{ s__('ClusterIntegration|Ingress IP Address') }} + <label for="ingress-endpoint"> + {{ s__('ClusterIntegration|Ingress Endpoint') }} </label> - <div v-if="ingressExternalIp" class="input-group"> + <div v-if="ingressExternalEndpoint" class="input-group"> <input - id="ingress-ip-address" - :value="ingressExternalIp" + id="ingress-endpoint" + :value="ingressExternalEndpoint" type="text" - class="form-control js-ip-address" + class="form-control js-endpoint" readonly /> <span class="input-group-append"> <clipboard-button - :text="ingressExternalIp" - :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" + :text="ingressExternalEndpoint" + :title="s__('ClusterIntegration|Copy Ingress Endpoint to clipboard')" class="input-group-text js-clipboard-btn" /> </span> </div> - <input v-else type="text" class="form-control js-ip-address" readonly value="?" /> + <input v-else type="text" class="form-control js-endpoint" readonly value="?" /> <p class="form-text text-muted"> {{ s__(`ClusterIntegration|Point a wildcard DNS to this - generated IP address in order to access - your application after it has been deployed.`) + generated endpoint in order to access + your application after it has been deployed.`) }} <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> {{ __('More information') }} @@ -321,19 +309,21 @@ export default { </p> </div> - <p v-if="!ingressExternalIp" class="settings-message js-no-ip-message"> + <p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message"> {{ - s__(`ClusterIntegration|The IP address is in - the process of being assigned. Please check your Kubernetes - cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) + s__(`ClusterIntegration|The endpoint is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} - <a :href="ingressHelpPath" target="_blank" rel="noopener noreferrer"> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> {{ __('More information') }} </a> </p> </template> - <div v-html="ingressDescription"></div> + <template v-if="!ingressInstalled"> + <div class="bs-callout bs-callout-info" v-html="ingressDescription"></div> + </template> </div> </application-row> <application-row @@ -443,7 +433,7 @@ export default { }} </p> - <template v-if="ingressExternalIp"> + <template v-if="ingressExternalEndpoint"> <div class="form-group"> <label for="jupyter-hostname"> {{ s__('ClusterIntegration|Jupyter Hostname') }} @@ -468,7 +458,7 @@ export default { <p v-if="ingressInstalled" class="form-text text-muted"> {{ s__(`ClusterIntegration|Replace this with your own hostname if you want. - If you do so, point hostname to Ingress IP Address from above.`) + If you do so, point hostname to Ingress IP Address from above.`) }} <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> {{ __('More information') }} @@ -493,10 +483,10 @@ export default { > <div slot="description"> <span v-if="!rbac"> - <p v-if="!rbac" class="bs-callout bs-callout-info append-bottom-0"> + <p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0"> {{ s__(`ClusterIntegration|You must have an RBAC-enabled cluster - to install Knative.`) + to install Knative.`) }} <a :href="helpPath" target="_blank" rel="noopener noreferrer"> {{ __('More information') }} @@ -534,31 +524,31 @@ export default { </template> <template v-if="knativeInstalled"> <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> - <label for="knative-ip-address"> + <label for="knative-endpoint"> <strong> {{ s__('ClusterIntegration|Knative Endpoint:') }} </strong> </label> - <div v-if="knativeExternalIp" class="input-group"> + <div v-if="knativeExternalEndpoint" class="input-group"> <input - id="knative-ip-address" - :value="knativeExternalIp" + id="knative-endpoint" + :value="knativeExternalEndpoint" type="text" - class="form-control js-knative-ip-address" + class="form-control js-knative-endpoint" readonly /> <span class="input-group-append"> <clipboard-button - :text="knativeExternalIp" + :text="knativeExternalEndpoint" :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')" - class="input-group-text js-knative-ip-clipboard-btn" + class="input-group-text js-knative-endpoint-clipboard-btn" /> </span> </div> <input v-else type="text" - class="form-control js-knative-ip-address" + class="form-control js-knative-endpoint" readonly value="?" /> @@ -576,13 +566,13 @@ export default { </p> <p - v-if="!knativeExternalIp" - class="settings-message js-no-knative-ip-message mt-2 mr-3 mb-0 ml-3 " + v-if="!knativeExternalEndpoint" + class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3" > {{ - s__(`ClusterIntegration|The IP address is in - the process of being assigned. Please check your Kubernetes - cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) + s__(`ClusterIntegration|The endpoint is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} </p> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 3f03a8512fc..92993337f02 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -25,6 +25,7 @@ export default class ClusterStore { requestStatus: null, requestReason: null, externalIp: null, + externalHostname: null, }, cert_manager: { title: s__('ClusterIntegration|Cert-Manager'), @@ -68,6 +69,7 @@ export default class ClusterStore { hostname: null, isEditingHostName: false, externalIp: null, + externalHostname: null, }, }, }; @@ -120,6 +122,7 @@ export default class ClusterStore { if (appId === INGRESS) { this.state.applications.ingress.externalIp = serverAppEntry.external_ip; + this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname; } else if (appId === CERT_MANAGER) { this.state.applications.cert_manager.email = this.state.applications.cert_manager.email || serverAppEntry.email; @@ -136,6 +139,8 @@ export default class ClusterStore { } this.state.applications.knative.externalIp = serverAppEntry.external_ip || this.state.applications.knative.externalIp; + this.state.applications.knative.externalHostname = + serverAppEntry.external_hostname || this.state.applications.knative.externalHostname; } else if (appId === RUNNER) { this.state.applications.runner.version = version; this.state.applications.runner.upgradeAvailable = upgradeAvailable; diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js new file mode 100644 index 00000000000..0b24d9fc920 --- /dev/null +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -0,0 +1,28 @@ +export default IssuableTokenKeys => { + const wipToken = { + key: 'wip', + type: 'string', + param: '', + symbol: '', + icon: 'admin', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: true, + capitalizeTokenValue: true, + }; + + IssuableTokenKeys.tokenKeys.push(wipToken); + IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken); + + const targetBranchToken = { + key: 'target-branch', + type: 'string', + param: '', + symbol: '', + icon: 'arrow-right', + tag: 'branch', + }; + + IssuableTokenKeys.tokenKeys.push(targetBranchToken); + IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken); +}; diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js new file mode 100644 index 00000000000..be867a3838d --- /dev/null +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -0,0 +1,164 @@ +import DropdownHint from './dropdown_hint'; +import DropdownUser from './dropdown_user'; +import DropdownNonUser from './dropdown_non_user'; +import DropdownEmoji from './dropdown_emoji'; +import NullDropdown from './null_dropdown'; +import DropdownAjaxFilter from './dropdown_ajax_filter'; +import DropdownUtils from './dropdown_utils'; +import { mergeUrlParams } from '../lib/utils/url_utility'; + +export default class AvailableDropdownMappings { + constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) { + this.container = container; + this.baseEndpoint = baseEndpoint; + this.groupsOnly = groupsOnly; + this.includeAncestorGroups = includeAncestorGroups; + this.includeDescendantGroups = includeDescendantGroups; + this.filteredSearchInput = this.container.querySelector('.filtered-search'); + } + + getAllowedMappings(supportedTokens) { + return this.buildMappings(supportedTokens, this.getMappings()); + } + + buildMappings(supportedTokens, availableMappings) { + const allowedMappings = { + hint: { + reference: null, + gl: DropdownHint, + element: this.container.querySelector('#js-dropdown-hint'), + }, + }; + + supportedTokens.forEach(type => { + if (availableMappings[type]) { + allowedMappings[type] = availableMappings[type]; + } + }); + + return allowedMappings; + } + + getMappings() { + return { + author: { + reference: null, + gl: DropdownUser, + element: this.container.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: DropdownUser, + element: this.container.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: DropdownNonUser, + extraArguments: { + endpoint: this.getMilestoneEndpoint(), + symbol: '%', + }, + element: this.container.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: DropdownNonUser, + extraArguments: { + endpoint: this.getLabelsEndpoint(), + symbol: '~', + preprocessing: DropdownUtils.duplicateLabelPreprocessing, + }, + element: this.container.querySelector('#js-dropdown-label'), + }, + 'my-reaction': { + reference: null, + gl: DropdownEmoji, + element: this.container.querySelector('#js-dropdown-my-reaction'), + }, + wip: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-wip'), + }, + confidential: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-confidential'), + }, + status: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-status'), + }, + type: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-type'), + }, + tag: { + reference: null, + gl: DropdownAjaxFilter, + extraArguments: { + endpoint: this.getRunnerTagsEndpoint(), + symbol: '~', + }, + element: this.container.querySelector('#js-dropdown-runner-tag'), + }, + 'target-branch': { + reference: null, + gl: DropdownNonUser, + extraArguments: { + endpoint: this.getMergeRequestTargetBranchesEndpoint(), + symbol: '', + }, + element: this.container.querySelector('#js-dropdown-target-branch'), + }, + }; + } + + getMilestoneEndpoint() { + return `${this.baseEndpoint}/milestones.json`; + } + + getLabelsEndpoint() { + let endpoint = `${this.baseEndpoint}/labels.json?`; + + if (this.groupsOnly) { + endpoint = `${endpoint}only_group_labels=true&`; + } + + if (this.includeAncestorGroups) { + endpoint = `${endpoint}include_ancestor_groups=true&`; + } + + if (this.includeDescendantGroups) { + endpoint = `${endpoint}include_descendant_groups=true`; + } + + return endpoint; + } + + getRunnerTagsEndpoint() { + return `${this.baseEndpoint}/admin/runners/tag_list.json`; + } + + getMergeRequestTargetBranchesEndpoint() { + const endpoint = `${gon.relative_url_root || + ''}/autocomplete/merge_request_target_branches.json`; + + const params = { + group_id: this.getGroupId(), + project_id: this.getProjectId(), + }; + + return mergeUrlParams(params, endpoint); + } + + getGroupId() { + return this.filteredSearchInput.getAttribute('data-group-id') || ''; + } + + getProjectId() { + return this.filteredSearchInput.getAttribute('data-project-id') || ''; + } +} diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 57847d4ad9f..cb0a84b490b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,14 +1,9 @@ +import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings'; import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; import FilteredSearchTokenKeys from './filtered_search_token_keys'; import DropdownUtils from './dropdown_utils'; -import DropdownHint from './dropdown_hint'; -import DropdownEmoji from './dropdown_emoji'; -import DropdownNonUser from './dropdown_non_user'; -import DropdownUser from './dropdown_user'; -import DropdownAjaxFilter from './dropdown_ajax_filter'; -import NullDropdown from './null_dropdown'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { @@ -50,114 +45,15 @@ export default class FilteredSearchDropdownManager { setupMapping() { const supportedTokens = this.filteredSearchTokenKeys.getKeys(); - const allowedMappings = { - hint: { - reference: null, - gl: DropdownHint, - element: this.container.querySelector('#js-dropdown-hint'), - }, - }; - const availableMappings = { - author: { - reference: null, - gl: DropdownUser, - element: this.container.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: DropdownUser, - element: this.container.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: DropdownNonUser, - extraArguments: { - endpoint: this.getMilestoneEndpoint(), - symbol: '%', - }, - element: this.container.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: DropdownNonUser, - extraArguments: { - endpoint: this.getLabelsEndpoint(), - symbol: '~', - preprocessing: DropdownUtils.duplicateLabelPreprocessing, - }, - element: this.container.querySelector('#js-dropdown-label'), - }, - 'my-reaction': { - reference: null, - gl: DropdownEmoji, - element: this.container.querySelector('#js-dropdown-my-reaction'), - }, - wip: { - reference: null, - gl: DropdownNonUser, - element: this.container.querySelector('#js-dropdown-wip'), - }, - confidential: { - reference: null, - gl: DropdownNonUser, - element: this.container.querySelector('#js-dropdown-confidential'), - }, - status: { - reference: null, - gl: NullDropdown, - element: this.container.querySelector('#js-dropdown-admin-runner-status'), - }, - type: { - reference: null, - gl: NullDropdown, - element: this.container.querySelector('#js-dropdown-admin-runner-type'), - }, - tag: { - reference: null, - gl: DropdownAjaxFilter, - extraArguments: { - endpoint: this.getRunnerTagsEndpoint(), - symbol: '~', - }, - element: this.container.querySelector('#js-dropdown-runner-tag'), - }, - }; - - supportedTokens.forEach(type => { - if (availableMappings[type]) { - allowedMappings[type] = availableMappings[type]; - } - }); - - this.mapping = allowedMappings; - } - - getMilestoneEndpoint() { - const endpoint = `${this.baseEndpoint}/milestones.json`; - - return endpoint; - } - - getLabelsEndpoint() { - let endpoint = `${this.baseEndpoint}/labels.json?`; - - if (this.groupsOnly) { - endpoint = `${endpoint}only_group_labels=true&`; - } - - if (this.includeAncestorGroups) { - endpoint = `${endpoint}include_ancestor_groups=true&`; - } - - if (this.includeDescendantGroups) { - endpoint = `${endpoint}include_descendant_groups=true`; - } - - return endpoint; - } + const availableMappings = new AvailableDropdownMappings( + this.container, + this.baseEndpoint, + this.groupsOnly, + this.includeAncestorGroups, + this.includeDescendantGroups, + ); - getRunnerTagsEndpoint() { - return `${this.baseEndpoint}/admin/runners/tag_list.json`; + this.mapping = availableMappings.getAllowedMappings(supportedTokens); } static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 33c82778c79..0c2e87521d9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -504,14 +504,7 @@ export default class FilteredSearchManager { const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); if (match) { - // Use lastIndexOf because the token key is allowed to contain underscore - // e.g. 'my_reaction' is the token key of 'my_reaction_emoji' - const lastIndexOf = keyParam.lastIndexOf('_'); - let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam; - // Replace underscore with hyphen in the sanitizedkey. - // e.g. 'my_reaction' => 'my-reaction' - sanitizedKey = sanitizedKey.replace('_', '-'); - const { symbol } = match; + const { key, symbol } = match; let quotationsToUse = ''; if (sanitizedValue.indexOf(' ') !== -1) { @@ -520,10 +513,10 @@ export default class FilteredSearchManager { } hasFilteredSearch = true; - const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); + const canEdit = this.canEdit && this.canEdit(key, sanitizedValue); const { uppercaseTokenName, capitalizeTokenValue } = match; FilteredSearchVisualTokens.addFilterVisualToken( - sanitizedKey, + key, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, { canEdit, diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 48534bdf815..11ed85504ec 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -88,21 +88,4 @@ export default class FilteredSearchTokenKeys { this.tokenKeys.push(confidentialToken); this.tokenKeysWithAlternative.push(confidentialToken); } - - addExtraTokensForMergeRequests() { - const wipToken = { - key: 'wip', - type: 'string', - param: '', - symbol: '', - icon: 'admin', - tag: 'Yes or No', - lowercaseValueOnSubmit: true, - uppercaseTokenName: true, - capitalizeTokenValue: true, - }; - - this.tokenKeys.push(wipToken); - this.tokenKeysWithAlternative.push(wipToken); - } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index addf1ad94df..315cd6f64da 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,10 +1,6 @@ -import _ from 'underscore'; -import AjaxCache from '~/lib/utils/ajax_cache'; +import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value'; import { objectToQueryString } from '~/lib/utils/common_utils'; -import Flash from '../flash'; import FilteredSearchContainer from './container'; -import UsersCache from '../lib/utils/users_cache'; -import DropdownUtils from './dropdown_utils'; export default class FilteredSearchVisualTokens { static getLastVisualTokenBeforeInput() { @@ -20,21 +16,6 @@ export default class FilteredSearchVisualTokens { }; } - /** - * Returns a computed API endpoint - * and query string composed of values from endpointQueryParams - * @param {String} endpoint - * @param {String} endpointQueryParams - */ - static getEndpointWithQueryParams(endpoint, endpointQueryParams) { - if (!endpointQueryParams) { - return endpoint; - } - - const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); - return `${endpoint}?${queryString}`; - } - static unselectTokens() { const otherTokens = FilteredSearchContainer.container.querySelectorAll( '.js-visual-token .selectable.selected', @@ -76,132 +57,33 @@ export default class FilteredSearchVisualTokens { `; } - static setTokenStyle(tokenContainer, backgroundColor, textColor) { - const token = tokenContainer; - - token.style.backgroundColor = backgroundColor; - token.style.color = textColor; - - if (textColor === '#FFFFFF') { - const removeToken = token.querySelector('.remove-token'); - removeToken.classList.add('inverted'); - } - - return token; - } - - static updateLabelTokenColor(tokenValueContainer, tokenValue) { - const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); - const { baseEndpoint } = filteredSearchInput.dataset; - const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( - `${baseEndpoint}/labels.json`, - filteredSearchInput.dataset.endpointQueryParams, - ); - - return AjaxCache.retrieve(labelsEndpoint) - .then(labels => { - const matchingLabel = (labels || []).find( - label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, - ); - - if (!matchingLabel) { - return; - } - - FilteredSearchVisualTokens.setTokenStyle( - tokenValueContainer, - matchingLabel.color, - matchingLabel.text_color, - ); - }) - .catch(() => new Flash('An error occurred while fetching label colors.')); - } - - static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { - const username = tokenValue.replace(/^@/, ''); - return ( - UsersCache.retrieve(username) - .then(user => { - if (!user) { - return; - } - - /* eslint-disable no-param-reassign */ - tokenValueContainer.dataset.originalValue = tokenValue; - tokenValueElement.innerHTML = ` - <img class="avatar s20" src="${user.avatar_url}" alt=""> - ${_.escape(user.name)} - `; - /* eslint-enable no-param-reassign */ - }) - // ignore error and leave username in the search bar - .catch(() => {}) - ); - } - - static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { - const container = tokenValueContainer; - const element = tokenValueElement; - const value = tokenValue; - - return ( - import(/* webpackChunkName: 'emoji' */ '../emoji') - .then(Emoji => { - Emoji.initEmojiMap() - .then(() => { - if (!Emoji.isEmojiNameValid(value)) { - return; - } - - container.dataset.originalValue = value; - element.innerHTML = Emoji.glEmojiTag(value); - }) - // ignore error and leave emoji name in the search bar - .catch(err => { - throw err; - }); - }) - // ignore error and leave emoji name in the search bar - .catch(importError => { - throw importError; - }) - ); - } - static renderVisualTokenValue(parentElement, tokenName, tokenValue) { + const tokenType = tokenName.toLowerCase(); const tokenValueContainer = parentElement.querySelector('.value-container'); const tokenValueElement = tokenValueContainer.querySelector('.value'); tokenValueElement.innerText = tokenValue; - if (['none', 'any'].includes(tokenValue.toLowerCase())) { - return; - } - - const tokenType = tokenName.toLowerCase(); + const visualTokenValue = new VisualTokenValue(tokenValue, tokenType); - if (tokenType === 'label') { - FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); - } else if (tokenType === 'author' || tokenType === 'assignee') { - FilteredSearchVisualTokens.updateUserTokenAppearance( - tokenValueContainer, - tokenValueElement, - tokenValue, - ); - } else if (tokenType === 'my-reaction') { - FilteredSearchVisualTokens.updateEmojiTokenAppearance( - tokenValueContainer, - tokenValueElement, - tokenValue, - ); - } + visualTokenValue.render(tokenValueContainer, tokenValueElement); } static addVisualTokenElement(name, value, options = {}) { - const { isSearchTerm = false, canEdit, uppercaseTokenName, capitalizeTokenValue } = options; + const { + isSearchTerm = false, + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + tokenClass = `search-token-${name.toLowerCase()}`, + } = options; const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); + if (!isSearchTerm) { + li.classList.add(tokenClass); + } + if (value) { li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ canEdit, @@ -328,6 +210,21 @@ export default class FilteredSearchVisualTokens { } } + /** + * Returns a computed API endpoint + * and query string composed of values from endpointQueryParams + * @param {String} endpoint + * @param {String} endpointQueryParams + */ + static getEndpointWithQueryParams(endpoint, endpointQueryParams) { + if (!endpointQueryParams) { + return endpoint; + } + + const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); + return `${endpoint}?${queryString}`; + } + static editToken(token) { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js new file mode 100644 index 00000000000..7f6f41c18f7 --- /dev/null +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -0,0 +1,125 @@ +import _ from 'underscore'; +import FilteredSearchContainer from '~/filtered_search/container'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import Flash from '~/flash'; +import UsersCache from '~/lib/utils/users_cache'; + +export default class VisualTokenValue { + constructor(tokenValue, tokenType) { + this.tokenValue = tokenValue; + this.tokenType = tokenType; + } + + render(tokenValueContainer, tokenValueElement) { + const { tokenType } = this; + + if (['none', 'any'].includes(tokenType)) { + return; + } + + if (tokenType === 'label') { + this.updateLabelTokenColor(tokenValueContainer); + } else if (tokenType === 'author' || tokenType === 'assignee') { + this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement); + } else if (tokenType === 'my-reaction') { + this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement); + } + } + + updateUserTokenAppearance(tokenValueContainer, tokenValueElement) { + const { tokenValue } = this; + const username = this.tokenValue.replace(/^@/, ''); + + return ( + UsersCache.retrieve(username) + .then(user => { + if (!user) { + return; + } + + /* eslint-disable no-param-reassign */ + tokenValueContainer.dataset.originalValue = tokenValue; + tokenValueElement.innerHTML = ` + <img class="avatar s20" src="${user.avatar_url}" alt=""> + ${_.escape(user.name)} + `; + /* eslint-enable no-param-reassign */ + }) + // ignore error and leave username in the search bar + .catch(() => {}) + ); + } + + updateLabelTokenColor(tokenValueContainer) { + const { tokenValue } = this; + const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); + const { baseEndpoint } = filteredSearchInput.dataset; + const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( + `${baseEndpoint}/labels.json`, + filteredSearchInput.dataset.endpointQueryParams, + ); + + return AjaxCache.retrieve(labelsEndpoint) + .then(labels => { + const matchingLabel = (labels || []).find( + label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, + ); + + if (!matchingLabel) { + return; + } + + VisualTokenValue.setTokenStyle( + tokenValueContainer, + matchingLabel.color, + matchingLabel.text_color, + ); + }) + .catch(() => new Flash('An error occurred while fetching label colors.')); + } + + static setTokenStyle(tokenValueContainer, backgroundColor, textColor) { + const token = tokenValueContainer; + + token.style.backgroundColor = backgroundColor; + token.style.color = textColor; + + if (textColor === '#FFFFFF') { + const removeToken = token.querySelector('.remove-token'); + removeToken.classList.add('inverted'); + } + + return token; + } + + updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement) { + const container = tokenValueContainer; + const element = tokenValueElement; + const value = this.tokenValue; + + return ( + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(Emoji => { + Emoji.initEmojiMap() + .then(() => { + if (!Emoji.isEmojiNameValid(value)) { + return; + } + + container.dataset.originalValue = value; + element.innerHTML = Emoji.glEmojiTag(value); + }) + // ignore error and leave emoji name in the search bar + .catch(err => { + throw err; + }); + }) + // ignore error and leave emoji name in the search bar + .catch(importError => { + throw importError; + }) + ); + } +} diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index de1ea0f58d6..fc73726857d 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -158,7 +158,6 @@ export default { href="#" title="Add reaction" > - <gl-loading-icon inline /> <icon css-classes="link-highlight award-control-icon-neutral" name="emoji_slightly_smiling_face" diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index 260484726f3..ff758fcb4fe 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -1,10 +1,11 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 339ce67438a..12a26fd88fa 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,10 +1,11 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index ec39db12e74..0bcca22e40f 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -2,12 +2,13 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { - IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 796ecbccde7..784eec1ea55 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -133,6 +133,10 @@ const bindEvents = () => { text: '.NET Core', icon: '.template-option .icon-dotnet', }, + android: { + text: 'Android', + icon: '.template-option svg.icon-android', + }, gomicro: { text: 'Go Micro', icon: '.template-option .icon-gomicro', diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 83ad8766cb5..d1cf2b8f9a0 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -3,7 +3,6 @@ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. *= require jquery.atwho - *= require select2 *= require_self *= require cropper.css */ @@ -18,6 +17,7 @@ @import "../../../node_modules/pikaday/scss/pikaday"; @import "../../../node_modules/dropzone/dist/basic"; +@import "../../../node_modules/select2/select2"; /* * GitLab UI framework diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e5b529ae11d..5bcfd5d1322 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -108,6 +108,8 @@ } .value-container { + display: flex; + align-items: center; background-color: $white-normal; color: $filter-value-text-color; border-radius: 0 2px 2px 0; @@ -121,7 +123,7 @@ .remove-token { display: inline-block; - padding-left: 4px; + padding-left: 8px; padding-right: 0; .fa-close { @@ -412,3 +414,10 @@ padding: 8px 16px; text-align: center; } + +.search-token-target-branch { + .value { + font-family: $monospace-font; + font-size: 13px; + } +} diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index ba3b0906e28..955ae80cd58 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -259,6 +259,7 @@ background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; + position: -webkit-sticky; position: sticky; top: $header-height; padding: $grid-size; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 3703b7568c8..53222a2bd4d 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -52,14 +52,16 @@ display: flex; flex-direction: row; - .btn + .btn { + .btn + .btn:not(.dropdown-toggle-split), + .btn + .btn-group { margin-left: $grid-size; } @include media-breakpoint-down(xs) { flex-direction: column; - .btn + .btn { + .btn + .btn:not(.dropdown-toggle-split), + .btn + .btn-group { margin-left: 0; margin-top: $grid-size; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e50db5310a6..c88922ae5ea 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -9,6 +9,7 @@ @media (min-width: map-get($grid-breakpoints, md)) { $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height; + position: -webkit-sticky; position: sticky; top: $mr-file-header-top; z-index: 102; @@ -725,6 +726,7 @@ } @include media-breakpoint-up(sm) { + position: -webkit-sticky; position: sticky; top: $header-height; background-color: $white-light; @@ -1015,6 +1017,7 @@ } .diff-tree-list { + position: -webkit-sticky; position: sticky; $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index e73d1a1289d..126b00af552 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -784,6 +784,7 @@ } @include media-breakpoint-up(md) { + position: -webkit-sticky; position: sticky; top: $header-height + $mr-tabs-height; width: 100%; @@ -810,6 +811,7 @@ border-bottom: 1px solid $border-color; @include media-breakpoint-up(sm) { + position: -webkit-sticky; position: sticky; } diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 0d5c8657c9e..091327931c2 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class AutocompleteController < ApplicationController - skip_before_action :authenticate_user!, only: [:users, :award_emojis] + skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches] def users project = Autocomplete::ProjectFinder @@ -38,4 +38,11 @@ class AutocompleteController < ApplicationController def award_emojis render json: AwardedEmojiFinder.new(current_user).execute end + + def merge_request_target_branches + merge_requests = MergeRequestsFinder.new(current_user, params).execute + target_branches = merge_requests.recent_target_branches + + render json: target_branches.map { |target_branch| { title: target_branch } } + end end diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 7276964b6e1..1fafc33e917 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -3,7 +3,6 @@ module Projects module Settings class OperationsController < Projects::ApplicationController - before_action :check_license before_action :authorize_update_environment! helper_method :error_tracking_setting @@ -65,10 +64,6 @@ module Projects ] } end - - def check_license - render_404 unless helpers.settings_operations_available? - end end end end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 93bee3f1488..84689ff5dc7 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -29,7 +29,7 @@ # class MergeRequestsFinder < IssuableFinder def self.scalar_params - @scalar_params ||= super + [:wip] + @scalar_params ||= super + [:wip, :target_branch] end def klass diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c5035797621..cd36c963ee5 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -69,7 +69,7 @@ class ApplicationSetting < ActiveRecord::Base url: true validates :admin_notification_email, - email: true, + devise_email: true, allow_blank: true validates :two_factor_grace_period, diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 7c15aaa4825..567f1a2267f 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -48,6 +48,7 @@ module Clusters def schedule_status_update return unless installed? return if external_ip + return if external_hostname ClusterWaitForIngressIpAddressWorker.perform_async(name, id) end diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 80205775b6a..7efcc175f9f 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -18,8 +18,10 @@ module Clusters def set_initial_status return unless not_installable? + return unless cluster&.application_ingress_available? - if cluster&.application_ingress_available? && cluster.application_ingress.external_ip + ingress = cluster.application_ingress + if ingress.external_ip || ingress.external_hostname self.status = 'installable' end end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 8d79b041b64..347c3c8c37f 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -66,6 +66,7 @@ module Clusters def schedule_status_update return unless installed? return if external_ip + return if external_hostname ClusterWaitForIngressIpAddressWorker.perform_async(name, id) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index be3e6a05e1e..5156c7d7514 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -67,6 +67,7 @@ module Clusters delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true delegate :available?, to: :application_knative, prefix: true, allow_nil: true delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true + delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true alias_attribute :base_domain, :domain diff --git a/app/models/email.rb b/app/models/email.rb index 3ce6e792fa8..7c33c5c7e64 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,7 +7,7 @@ class Email < ActiveRecord::Base belongs_to :user validates :user_id, presence: true - validates :email, presence: true, uniqueness: true, email: true + validates :email, presence: true, uniqueness: true, devise_email: true validate :unique_email, if: ->(email) { email.email_changed? } scope :confirmed, -> { where.not(confirmed_at: nil) } diff --git a/app/models/member.rb b/app/models/member.rb index 8e071a8ff21..5dbc0c2eec9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -28,7 +28,7 @@ class Member < ActiveRecord::Base presence: { if: :invite? }, - email: { + devise_email: { allow_nil: true }, uniqueness: { diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index acf80addc6a..af8cb37bfb6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -203,6 +203,22 @@ class MergeRequest < ActiveRecord::Base '!' end + # Returns the top 100 target branches + # + # The returned value is a Array containing branch names + # sort by updated_at of merge request: + # + # ['master', 'develop', 'production'] + # + # limit - The maximum number of target branch to return. + def self.recent_target_branches(limit: 100) + group(:target_branch) + .select(:target_branch) + .reorder('MAX(merge_requests.updated_at) DESC') + .limit(limit) + .pluck(:target_branch) + end + def rebase_in_progress? strong_memoize(:rebase_in_progress) do # The source project can be deleted diff --git a/app/models/user.rb b/app/models/user.rb index 778c9e631bd..0ebfb9a0ccb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -162,9 +162,9 @@ class User < ApplicationRecord validates :name, presence: true validates :email, confirmation: true validates :notification_email, presence: true - validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } - validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true - validates :commit_email, email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email } + validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email } + validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true + validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email } validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index ecb2797d1d9..537319addc2 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -17,6 +17,7 @@ class IssuablePolicy < BasePolicy enable :reopen_issue enable :read_merge_request enable :update_merge_request + enable :reopen_merge_request end rule { locked & ~is_project_member }.policy do diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index a2950951d03..a3692857ff4 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true class MergeRequestPolicy < IssuablePolicy + rule { locked }.policy do + prevent :reopen_merge_request + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index cf257ed47c8..9f9f5230040 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -231,6 +231,7 @@ class ProjectPolicy < BasePolicy enable :admin_merge_request enable :admin_milestone enable :update_merge_request + enable :reopen_merge_request enable :create_commit_status enable :update_commit_status enable :create_build diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index 02df1480828..a4a2c015c4e 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -6,6 +6,7 @@ class ClusterApplicationEntity < Grape::Entity expose :status_reason expose :version expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } + expose :external_hostname, if: -> (e, _) { e.respond_to?(:external_hostname) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } diff --git a/app/services/clusters/applications/check_ingress_ip_address_service.rb b/app/services/clusters/applications/check_ingress_ip_address_service.rb index 0ec06e776a7..e254a0358a0 100644 --- a/app/services/clusters/applications/check_ingress_ip_address_service.rb +++ b/app/services/clusters/applications/check_ingress_ip_address_service.rb @@ -11,9 +11,13 @@ module Clusters def execute return if app.external_ip + return if app.external_hostname return unless try_obtain_lease - app.update!(external_ip: ingress_ip) if ingress_ip + app.external_ip = ingress_ip if ingress_ip + app.external_hostname = ingress_hostname if ingress_hostname + + app.save! if app.changed? end private @@ -25,12 +29,16 @@ module Clusters end def ingress_ip - service.status.loadBalancer.ingress&.first&.ip + ingress_service&.ip + end + + def ingress_hostname + ingress_service&.hostname end - def service + def ingress_service strong_memoize(:ingress_service) do - app.ingress_service + app.ingress_service.status.loadBalancer.ingress&.first end end end diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index f6cbe769ef4..f87005bcb6c 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -3,7 +3,7 @@ module MergeRequests class ReopenService < MergeRequests::BaseService def execute(merge_request) - return merge_request unless can?(current_user, :update_merge_request, merge_request) + return merge_request unless can?(current_user, :reopen_merge_request, merge_request) if merge_request.reopen create_event(merge_request) diff --git a/app/validators/devise_email_validator.rb b/app/validators/devise_email_validator.rb new file mode 100644 index 00000000000..6ca921ca7fa --- /dev/null +++ b/app/validators/devise_email_validator.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# DeviseEmailValidator +# +# Custom validator for email formats. It asserts that there are no +# @ symbols or whitespaces in either the localpart or the domain, and that +# there is a single @ symbol separating the localpart and the domain. +# +# The available options are: +# - regexp: Email regular expression used to validate email formats as instance of Regexp class. +# If provided value has different type then a new Rexexp class instance is created using the value. +# Default: +Devise.email_regexp+ +# +# Example: +# class User < ActiveRecord::Base +# validates :personal_email, devise_email: true +# +# validates :public_email, devise_email: { regexp: Devise.email_regexp } +# end +class DeviseEmailValidator < ActiveModel::EachValidator + DEFAULT_OPTIONS = { + regexp: Devise.email_regexp + }.freeze + + def initialize(options) + options.reverse_merge!(DEFAULT_OPTIONS) + + raise ArgumentError, "Expected 'regexp' argument of type class Regexp" unless options[:regexp].is_a?(Regexp) + + super(options) + end + + def validate_each(record, attribute, value) + record.errors.add(attribute, :invalid) unless value =~ options[:regexp] + end +end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb deleted file mode 100644 index 9459edb7515..00000000000 --- a/app/validators/email_validator.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp - end -end diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml index 7c04ef03947..99d8af65068 100644 --- a/app/views/admin/deploy_keys/edit.html.haml +++ b/app/views/admin/deploy_keys/edit.html.haml @@ -1,10 +1,10 @@ -- page_title 'Edit Deploy Key' -%h3.page-title Edit public deploy key +- page_title _('Edit Deploy Key') +%h3.page-title= _('Edit public deploy key') %hr %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Save changes', class: 'btn-success btn' - = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' + = f.submit _('Save changes'), class: 'btn-success btn' + = link_to _('Cancel'), admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 01013be06d6..9fffa97f969 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,19 +1,19 @@ -- page_title "Deploy Keys" +- page_title _('Deploy Keys') %h3.page-title.deploy-keys-title - Public deploy keys (#{@deploy_keys.count}) + = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.count } .float-right - = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted' + = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted' - if @deploy_keys.any? .table-holder.deploy-keys-list %table.table %thead %tr - %th.col-sm-2 Title - %th.col-sm-4 Fingerprint - %th.col-sm-2 Projects with write access - %th.col-sm-2 Added at + %th.col-sm-2= _('Title') + %th.col-sm-4= _('Fingerprint') + %th.col-sm-2= _('Projects with write access') + %th.col-sm-2= _('Added at') %th.col-sm-2 %tbody - @deploy_keys.each do |deploy_key| @@ -27,8 +27,8 @@ = link_to project.full_name, admin_project_path(project), class: 'label deploy-project-label' %td %span.cgray - added #{time_ago_with_tooltip(deploy_key.created_at)} + = _('added %{created_at_timeago}').html_safe % { created_at_timeago: time_ago_with_tooltip(deploy_key.created_at) } %td .float-right - = link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm' - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key' + = link_to _('Edit'), edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm' + = link_to _('Remove'), admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-sm btn-remove delete-key' diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 9fb91a39387..c08b41e2f23 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -37,7 +37,7 @@ = s_('ClusterIntegration|Alternatively') %code #{@cluster.application_ingress_external_ip}.nip.io = s_('ClusterIntegration| can be used instead of a custom domain.') - - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip') + - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-external-endpoint') - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url } = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe } diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 7d381c6d4a6..884fa323093 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -22,8 +22,8 @@ cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), - ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), - ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'), + ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-endpoint'), + ingress_dns_help_path: help_page_path('user/project/clusters/index.md', anchor: 'manually-determining-the-external-endpoint'), manage_prometheus_path: manage_prometheus_path } } .js-cluster-application-notice diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index 4dbda5c754b..b1c192d7bad 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -5,7 +5,7 @@ = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - page_title "Activity" - header_title "Activity", activity_dashboard_path diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 19b06ba5cdd..d1d8d970b59 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,7 +2,7 @@ - page_title "Groups" - header_title "Groups", dashboard_groups_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) = render 'dashboard/groups_head' - if params[:filter].blank? && @groups.empty? diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index afd46412fab..352ec986ccb 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,7 +4,7 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues") -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) .page-title-holder %h1.page-title= _('Issues') diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 3e5f13b92e3..659cc254b10 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,7 +2,7 @@ - page_title _("Merge Requests") - @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username) -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) .page-title-holder %h1.page-title= _('Merge Requests') diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 446b4715b2d..dc9468b3368 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -4,7 +4,7 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - page_title "Projects" - header_title "Projects", dashboard_projects_path diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 3a45f6df017..a0d85446e5f 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -4,7 +4,7 @@ - page_title _("Starred Projects") - header_title _("Projects"), dashboard_projects_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) %div{ class: container_class } = render "projects/last_push" diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 47729321961..c569bc682a6 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -2,7 +2,7 @@ - page_title "Todos" - header_title "Todos", dashboard_todos_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) .page-title-holder %h1.page-title= _('Todos') diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 869be4e8581..fd86d07fc86 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,7 +2,7 @@ - page_title _("Groups") - header_title _("Groups"), dashboard_groups_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - if current_user = render 'dashboard/groups_head' diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index d18dec7bd8e..dd2bf6a5ef8 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -2,7 +2,7 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - if current_user = render 'dashboard/projects_head' diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index d18dec7bd8e..dd2bf6a5ef8 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -2,7 +2,7 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - if current_user = render 'dashboard/projects_head' diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index d18dec7bd8e..dd2bf6a5ef8 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -2,7 +2,7 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= render_if_exists "shared/gold_trial_callout" += render_dashboard_gold_trial(current_user) - if current_user = render 'dashboard/projects_head' diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index bd6f1c05949..57fbd360d46 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -1,5 +1,5 @@ %ul.content-list.mr-list.issuable-list - - if @merge_requests.exists? + - if @merge_requests.present? = render @merge_requests - else = render 'shared/empty_states/merge_requests' diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 3cd83feb842..70011d58c8a 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -1,4 +1,5 @@ - can_update_merge_request = can?(current_user, :update_merge_request, @merge_request) +- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request) - if @merge_request.closed_without_fork? .alert.alert-danger @@ -33,10 +34,11 @@ - if can_update_merge_request %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' + - if can_reopen_merge_request %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' - if can_update_merge_request = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit qa-edit-button" - = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request + = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_reopen_merge_request diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml index eb7808573b9..bc0dc7f9631 100644 --- a/app/views/shared/deploy_keys/_form.html.haml +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -13,9 +13,10 @@ = form.label :key, class: 'col-form-label col-sm-2' .col-sm-10 %p.light - Paste a machine public key here. Read more about how to generate it - = link_to 'here', help_page_path('ssh/README') - = form.text_area :key, class: 'form-control thin-area', rows: 5 + - link_start = "<a href='#{help_page_path('ssh/README')}' target='_blank' rel='noreferrer noopener'>".html_safe + - link_end = '</a>' + = _('Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}').html_safe % { link_start: link_start, link_end: link_end.html_safe } + = form.text_area :key, class: 'form-control thin_area', rows: 5 - else = form.label :fingerprint, class: 'col-form-label col-sm-2' .col-sm-10 @@ -28,6 +29,6 @@ .col-sm-10 = deploy_keys_project_form.label :can_push do = deploy_keys_project_form.check_box :can_push - %strong Write access allowed + %strong= _('Write access allowed') %p.light.append-bottom-0 - Allow this key to push to repository as well? (Default only allows pull access.) + = _('Allow this key to push to repository as well? (Default only allows pull access.)') diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index bdba47ed14d..2bcfcb6fa7c 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -71,6 +71,7 @@ = render 'shared/issuable/user_dropdown_item', user: User.new(username: '{{username}}', name: '{{name}}'), avatar: { lazy: true, url: '{{avatar_url}}' } + = render_if_exists 'shared/issuable/approver_dropdown' #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'None' } } @@ -136,6 +137,11 @@ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } %button.btn.btn-link{ type: 'button' } = _('No') + #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value.monospace + {{title}} = render_if_exists 'shared/issuable/filter_weight', type: type diff --git a/changelogs/unreleased/20084-update-the-spinner-component.yml b/changelogs/unreleased/20084-update-the-spinner-component.yml index c93648e4f54..46d79767753 100644 --- a/changelogs/unreleased/20084-update-the-spinner-component.yml +++ b/changelogs/unreleased/20084-update-the-spinner-component.yml @@ -1,5 +1,5 @@ --- -title: Add a spinner icon which is rendered using pure css -merge_request: 25186 +title: Add a spinner icon which is rendered using pure css + update conflicting CSS +merge_request: 25683 author: type: changed diff --git a/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml b/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml new file mode 100644 index 00000000000..04dbc3a1d5a --- /dev/null +++ b/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml @@ -0,0 +1,5 @@ +--- +title: Align EmailValidator to validate_email gem implementation +merge_request: 24971 +author: Horatiu Eugen Vlad +type: fixed diff --git a/changelogs/unreleased/56864-reopen-locked-mr.yml b/changelogs/unreleased/56864-reopen-locked-mr.yml new file mode 100644 index 00000000000..d1d71531ac8 --- /dev/null +++ b/changelogs/unreleased/56864-reopen-locked-mr.yml @@ -0,0 +1,5 @@ +--- +title: Disallow reopening of a locked merge request +merge_request: 24882 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/58649-project-template-for-android.yml b/changelogs/unreleased/58649-project-template-for-android.yml new file mode 100644 index 00000000000..130992272ec --- /dev/null +++ b/changelogs/unreleased/58649-project-template-for-android.yml @@ -0,0 +1,5 @@ +--- +title: Add project template for Android +merge_request: 25870 +author: +type: changed diff --git a/changelogs/unreleased/deploy-keys-ext.yml b/changelogs/unreleased/deploy-keys-ext.yml new file mode 100644 index 00000000000..e1d2fe08425 --- /dev/null +++ b/changelogs/unreleased/deploy-keys-ext.yml @@ -0,0 +1,5 @@ +--- +title: Externalize admin deploy keys strings +merge_request: +author: +type: other diff --git a/changelogs/unreleased/filter-merge-requests-by-target-branch.yml b/changelogs/unreleased/filter-merge-requests-by-target-branch.yml new file mode 100644 index 00000000000..d0aba631c96 --- /dev/null +++ b/changelogs/unreleased/filter-merge-requests-by-target-branch.yml @@ -0,0 +1,5 @@ +--- +title: Add target branch filter to merge requests search bar +merge_request: 24380 +author: Hiroyuki Sato +type: added diff --git a/changelogs/unreleased/ingress-hostnames.yml b/changelogs/unreleased/ingress-hostnames.yml new file mode 100644 index 00000000000..66721113769 --- /dev/null +++ b/changelogs/unreleased/ingress-hostnames.yml @@ -0,0 +1,5 @@ +--- +title: Added support for ingress hostnames +merge_request: 25181 +author: walkafwalka +type: added diff --git a/config/routes.rb b/config/routes.rb index 53c6225eff1..bbf00208545 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,7 @@ Rails.application.routes.draw do get '/autocomplete/users/:id' => 'autocomplete#user' get '/autocomplete/projects' => 'autocomplete#projects' get '/autocomplete/award_emojis' => 'autocomplete#award_emojis' + get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches' # Search get 'search' => 'search#show' diff --git a/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb new file mode 100644 index 00000000000..2c3a54b12a9 --- /dev/null +++ b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddExternalHostnameToIngressAndKnative < ActiveRecord::Migration[5.0] + DOWNTIME = false + + def change + add_column :clusters_applications_ingress, :external_hostname, :string + add_column :clusters_applications_knative, :external_hostname, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c782524c391..59a76e21a5f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190301081611) do +ActiveRecord::Schema.define(version: 20190301182457) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -708,6 +708,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do t.string "cluster_ip" t.text "status_reason" t.string "external_ip" + t.string "external_hostname" t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true, using: :btree end @@ -733,6 +734,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do t.string "hostname" t.text "status_reason" t.string "external_ip" + t.string "external_hostname" t.index ["cluster_id"], name: "index_clusters_applications_knative_on_cluster_id", unique: true, using: :btree end diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 78ebf8a083b..b1aaa3bca13 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -39,6 +39,8 @@ options: ### Improving NFS performance with GitLab +NOTE: **Note:** This is only available with GitLab 11.9 and up. + If you are using NFS to share Git data, we recommend that you enable a number of feature flags that will allow GitLab application processes to access Git data directly instead of going through the [Gitaly diff --git a/doc/ci/README.md b/doc/ci/README.md index ab77c63f794..c66a1d4b7a8 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -5,76 +5,62 @@ description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Inte # GitLab Continuous Integration (GitLab CI/CD) -**GitLab CI/CD** is GitLab's built-in tool for software development using the Continuous Methodology (Continuous Integration, Continuous Delivery, Continuous Deployment). +GitLab CI/CD is GitLab's built-in tool for software development using continuous methodology: -## Overview - -CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you: +- Continuous integration (CI). +- Continuous delivery and deployment (CD). -| Level of expertise | Resource | -|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| -| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). | -| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [quick start guide](quick_start/README.md). | -| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. | - -NOTE: **Note:** Within the [DevOps lifecycle](../README.md#the-entire-devops-lifecycle), GitLab CI/CD spans the [Verify (CI)](../README.md#verify) and [Release (CD)](../README.md#release) stages. -## Essentials +## Overview -The following documentation provides the minimum required knowledge for making use of GitLab CI/CD: +CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you: -| Topic | Description | -|:------------------------------------------------------------------------|:---------------------------------------------------------| -| [Getting started with GitLab CI/CD](quick_start/README.md) | Outlines the first steps for configuring GitLab CI/CD. | -| [Introduction to pipelines and jobs](pipelines.md) | Provides an overview of GitLab CI/CD and jobs. | -| [Configuration of your pipelines with `.gitlab-ci.yml`](yaml/README.md) | A comprehensive reference for the `.gitlab-ci.yml` file. | -| [`.gitlab-ci.yml` introduction](../user/project/pages/getting_started_part_four.md) | A step-by-step introduction to writing a GitLab CI/CD configuration file (`.gitlab-ci.yml`) for the first time. | +| Level of expertise | Resource | +|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------| +| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). | +| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [getting started guide](quick_start/README.md). | +| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. | -NOTE: **Note:** -Familiarity with [GitLab Runner](https://docs.gitlab.com/runner/) is useful because it is -responsible for running the jobs in your CI/CD pipeline. On GitLab.com, shared Runners are enabled -by default so you don't need to set up anything to get started. +Familiarity with GitLab Runner is also useful because it is responsible for running the jobs in your +CI/CD pipeline. On GitLab.com, shared Runners are enabled by default so you won't need to set this up to get started. -### Auto DevOps +## CI/CD with Auto DevOps -An alternative to manually configuring CI/CD, GitLab supports [Auto DevOps](../topics/autodevops/index.md), -which: +[Auto DevOps](../topics/autodevops/index.md) is the default minimum-configuration method for +implementing CI/CD. Auto DevOps: - Provides simplified setup and execution of CI/CD. - Allows GitLab to automatically detect, build, test, deploy, and monitor your applications. -## Basic usage +## Manually configured CI/CD + +For complete control, you can manually configure GitLab CI/CD. + +### Usage With basic knowledge of how GitLab CI/CD works, the following documentation extends your knowledge into more features: -| Topic | Description | -|:-------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------| -| [CI/CD Variables](variables/README.md) | How environment variables can be configured and made available in pipelines. | -| [Where variables can be used](variables/where_variables_can_be_used.md) | A deeper look into where and how CI/CD variables can be used. | -| [User](../user/permissions.md#gitlab-ci) and [job](../user/permissions.md#job-permissions) permissions | Learn about the access levels a user can have for performing certain CI actions. | -| [Configuring GitLab Runners](runners/README.md) | Documentation for configuring [GitLab Runner](https://docs.gitlab.com/runner/). | - -## Advanced usage - -Once you get familiar with the basics of GitLab CI/CD, consult the following documentation to make -use of advanced features: - -| Topic | Description | -|:---------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| -| [Introduction to environments and deployments](environments.md) | Learn how to separate your jobs into environments and use them for different purposes like testing, building and, deploying. | -| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Learn about the output of jobs. | -| [Cache dependencies in GitLab CI/CD](caching/index.md) | Discover how to speed up pipelines using caching. | -| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. | -| [Pipelines for merge requests](merge_request_pipelines/index.md) | Create pipelines specifically for merge requests. | -| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. | -| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. | -| [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. | -| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. | -| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | -| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | +| Topic | Description | +|:-------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| +| [Introduction to pipelines and jobs](pipelines.md) | Provides an overview of GitLab CI/CD and jobs. | +| [CI/CD Variables](variables/README.md) | How environment variables can be configured and made available in pipelines. | +| [Where variables can be used](variables/where_variables_can_be_used.md) | A deeper look into where and how CI/CD variables can be used. | +| [User](../user/permissions.md#gitlab-ci) and [job](../user/permissions.md#job-permissions) permissions | Learn about the access levels a user can have for performing certain CI actions. | +| [Configuring GitLab Runners](runners/README.md) | Documentation for configuring [GitLab Runner](https://docs.gitlab.com/runner/). | +| [Introduction to environments and deployments](environments.md) | Learn how to separate your jobs into environments and use them for different purposes like testing, building and, deploying. | +| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Learn about the output of jobs. | +| [Cache dependencies in GitLab CI/CD](caching/index.md) | Discover how to speed up pipelines using caching. | +| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. | +| [Pipelines for merge requests](merge_request_pipelines/index.md) | Create pipelines specifically for merge requests. | +| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. | +| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. | +| [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. | +| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. | +| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | +| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | ### GitLab Pages @@ -82,12 +68,16 @@ GitLab CI/CD can be used to build and host static websites. For more information documentation on [GitLab Pages](../user/project/pages/index.md), or dive right into the [CI/CD step-by-step guide for Pages](../user/project/pages/getting_started_part_four.md). -## Examples +### Examples + +GitLab provides examples of configuring GitLab CI/CD in the form of: -Check out the [GitLab CI/CD examples](examples/README.md) for a collection of tutorials and guides on -setting up your CI/CD pipeline for various programming languages, frameworks, and operating systems. +- A collection of [examples and other resources](examples/README.md). +- Example projects that are available at the [`gitlab-examples`](https://gitlab.com/gitlab-examples) group. For example, see: + - [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines. + - [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps. -## Administration +### Administration As a GitLab administrator, you can change the default behavior of GitLab CI/CD for: @@ -99,7 +89,7 @@ See also: - [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md). - Other [CI administration settings](../administration/index.md#continuous-integration-settings). -## Using Docker +### Using Docker Docker is commonly used with GitLab CI/CD. Learn more about how to to accomplish this with the following documentation: @@ -113,58 +103,13 @@ Related topics include: - [Docker integration](docker/README.md). - [CI services (linked Docker containers)](services/README.md). -- [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/) (article). - -## Further resources - -This section provides further resources to help you get familiar with GitLab CI/CD. - -### Articles - -The following table provides a list of articles about CI/CD, sorted in reverse chronological order of publish date: - -| Publish Date | Article | -|:-------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2017-07-13 | [Making CI easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/). | -| 2017-05-22 | [Fast and natural continuous integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/). | -| 2016-11-22 | [Introducing Review Apps](https://about.gitlab.com/2016/11/22/introducing-review-apps/). | -| 2016-08-26 | [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). | -| 2016-08-05 | [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/). | -| 2016-07-29 | [GitLab CI: Run jobs sequentially, in parallel or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/). | -| 2016-06-09 | [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) | -| 2016-05-23 | [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/). | -| 2016-05-05 | [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/) | -| 2016-04-19 | [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/) | -| 2015-03-01 | [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/). | -| 2015-12-14 | [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/). | - -### Videos - -The following table provides a list of videos about CI/CD, sorted in reverse chronological order of publish date: - -| Publish Date | Video | -|:-------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2017-07-17 | [GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195). | -| 2017-03-13 | [Demo: CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/). | -| 2016-04-20 | [Webcast Recording and Slides: Getting started with CI in GitLab](https://about.gitlab.com/2016/04/20/webcast-recording-and-slides-introduction-to-ci-in-gitlab/). | - -In addition, the following third-party videos are available: - -- [Intégration continue avec GitLab (September 2016)](https://www.youtube.com/watch?v=URcMBXjIr24&t=13s). -- [GitLab CI for Minecraft Plugins (July 2016)](https://www.youtube.com/watch?v=Z4pcI9F8yf8). - -### Example Projects - -[`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps. - -Other example projects are available at the [`gitlab-examples`](https://gitlab.com/gitlab-examples) group. -### Why GitLab CI/CD? +## Why GitLab CI/CD? -The following articles explain reasons why you might use GitLab CI/CD for your CI/CD infrastructure: +The following articles explain reasons to use GitLab CI/CD for your CI/CD infrastructure: -- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/). -- [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/). +- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/) +- [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/) See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation. diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 87e86bef44b..a1c997d1de6 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -4,88 +4,114 @@ comments: false # GitLab CI/CD Examples -A collection of [`.gitlab-ci.yml` template files][gitlab-ci-templates] is maintained in GitLab. When you create a new file via the UI, -GitLab will give you the option to choose one of these templates. -If your favorite programming language or framework are missing we would love your -help by sending a merge request with a new `.gitlab-ci.yml` to this project. - -There's also a collection of repositories with [example projects](https://gitlab.com/gitlab-examples) for various languages. You can fork and adjust them to your own needs. - -## Languages, frameworks, OSs - -- **PHP**: - - [Testing a PHP application](php.md) - - [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md) - - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) -- **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) -- **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) -- **Java**: - - [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md) - - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) -- **Scala**: [Test a Scala application](test-scala-application.md) -- **Clojure**: [Test a Clojure application](test-clojure-application.md) -- **Elixir**: - - [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md) - - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/) -- **iOS and macOS**: - - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) - - [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) -- **Android**: [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/) -- **Debian**: [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) -- **Maven**: [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md) - -### Game development - -- [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md) - -### Miscellaneous - -- [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md) -- [Using `dpl` as deployment tool](deployment/README.md) -- [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) - -## Test Reports +Examples are a useful way of understanding how to implement GitLab CI/CD for your specific use case. + +Examples are available in several forms. As a collection of: + +- `.gitlab-ci.yml` [template files](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates) maintained in GitLab. When you create a new file via the UI, + GitLab will give you the option to choose one of these templates. This will allow you to quickly bootstrap your project for CI/CD. + If your favorite programming language or framework are missing, we would love your help by sending a merge request with a new `.gitlab-ci.yml` to this project. +- Repositories with [example projects](https://gitlab.com/gitlab-examples) for various languages. You can fork and adjust them to your own needs. +- Examples and [other resources](#other-resources) listed below. + +## CI/CD examples + +The following table lists examples for different use cases: + +| Use case | Resource | +|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------| +| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). | +| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). | +| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** | +| Container scanning | [Container Scanning with GitLab CI/CD](container_scanning.md). | +| Dependency scanning | [Dependency Scanning with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html). **[ULTIMATE]** | +| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). | +| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](dast.md) **[ULTIMATE]** | +| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). | +| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). | +| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. | +| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). | +| JUnit | [JUnit test reports](../junit_test_reports.md). | +| License management | [Dependencies license management with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/license_management.html) **[ULTIMATE]** | +| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). | +| PHP | [Testing PHP projects](php.md). | +| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). | +| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). | +| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). | +| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). | +| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). | +| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/sast.html) **[ULTIMATE]** | +| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). | + +### Contributing examples + +Contributions are welcome! You can help your favorite programming +language users and GitLab by sending a merge request with a guide for that language. +You may want to apply for the [GitLab Community Writers Program](https://about.gitlab.com/community-writers/) +to get paid for writing complete articles for GitLab. -[Collect test reports in Verify stage](../junit_test_reports.md) +## Other resources -## Code Quality analysis +This section provides further resources to help you get familiar with different aspects of GitLab CI/CD. -**(Starter)** [Analyze your project's Code Quality](code_quality.md) +NOTE: **Note:** +These resources may no longer reflect the current state of GitLab CI/CD. -## Static Application Security Testing (SAST) +### CI/CD in the cloud -**(Ultimate)** [Scan your code for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/sast.html) +For examples of setting up GitLab CI/CD for cloud-based environments, see: -## Dependency Scanning +- [How to set up multi-account AWS SAM deployments with GitLab CI](https://about.gitlab.com/2019/02/04/multi-account-aws-sam-deployments-with-gitlab-ci/) +- [How to autoscale continuous deployment with GitLab Runner on DigitalOcean](https://about.gitlab.com/2018/06/19/autoscale-continuous-deployment-gitlab-runner-digital-ocean/) +- [How to create a CI/CD pipeline with Auto Deploy to Kubernetes using GitLab and Helm](https://about.gitlab.com/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/) -**(Ultimate)** [Scan your dependencies for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) +### Customer stories -## Container Scanning +For some customer experiences with GitLab CI/CD, see: -[Scan your Docker images for vulnerabilities](container_scanning.md) +- [How Verizon Connect reduced datacenter deploys from 30 days to under 8 hours with GitLab](https://about.gitlab.com/2019/02/14/verizon-customer-story/) +- [How Wag! cut their release process from 40 minutes to just 6](https://about.gitlab.com/2019/01/16/wag-labs-blog-post/) +- [How Jaguar Land Rover embraced CI to speed up their software lifecycle](https://about.gitlab.com/2018/07/23/chris-hill-devops-enterprise-summit-talk/) -## Dynamic Application Security Testing (DAST) +### Getting started -Scan your app for vulnerabilities with GitLab [Dynamic Application Security Testing (DAST)](dast.md) +For some examples to help get you started, see: -## Browser Performance Testing with Sitespeed.io +- [GitLab CI/CD's 2018 highlights](https://about.gitlab.com/2019/01/21/gitlab-ci-cd-features-improvements/) +- [A beginner's guide to continuous integration](https://about.gitlab.com/2018/01/22/a-beginners-guide-to-continuous-integration/) +- [Making CI easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) -Analyze your [browser performance with Sitespeed.io](browser_performance.md) +### Implementing GitLab CI/CD -## GitLab CI/CD for Review Apps +For examples of others who have implemented GitLab CI/CD, see: -- [Example project](https://gitlab.com/gitlab-examples/review-apps-nginx/) that shows how to use GitLab CI/CD for [Review Apps](../review_apps/index.html) +- [How to streamline interactions between multiple repositories with multi-project pipelines](https://about.gitlab.com/2018/10/31/use-multiproject-pipelines-with-gitlab-cicd/) +- [How we used GitLab CI to build GitLab faster](https://about.gitlab.com/2018/05/02/using-gitlab-ci-to-build-gitlab-faster/) +- [Test all the things in GitLab CI with Docker by example](https://about.gitlab.com/2018/02/05/test-all-the-things-gitlab-ci-docker-examples/) +- [A Craftsman looks at continuous integration](https://about.gitlab.com/2018/01/17/craftsman-looks-at-continuous-integration/) +- [Go tools and GitLab: How to do continuous integration like a boss](https://about.gitlab.com/2017/11/27/go-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss/) +- [GitBot – automating boring Git operations with CI](https://about.gitlab.com/2017/11/02/automating-boring-git-operations-gitlab-ci/) +- [How to use GitLab CI for Vue.js](https://about.gitlab.com/2017/09/12/vuejs-app-gitlab/) +- Video: [GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195) - [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) +- [Fast and natural continuous integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) +- [Demo: CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/) -## GitLab CI/CD for GitLab Pages +### Integrating GitLab CI/CD with other systems -See the documentation on [GitLab Pages](../../user/project/pages/index.md) for a complete overview. +To see how you can integrate GitLab CI/CD with third-party systems, see: -## Contributing +- [Streamline and shorten error remediation with Sentry’s new GitLab integration](https://about.gitlab.com/2019/01/25/sentry-integration-blog-post/) +- [How to simplify your smart home configuration with GitLab CI/CD](https://about.gitlab.com/2018/08/02/using-the-gitlab-ci-slash-cd-for-smart-home-configuration-management/) +- [Demo: GitLab + Jira + Jenkins](https://about.gitlab.com/2018/07/30/gitlab-workflow-with-jira-jenkins/) +- [Introducing Auto Breakfast from GitLab (sort of)](https://about.gitlab.com/2018/06/29/introducing-auto-breakfast-from-gitlab/) -Contributions are very welcome! You can help your favorite programming -language users and GitLab by sending a merge request with a guide for that language. -You may want to apply for the [GitLab Community Writers Program](https://about.gitlab.com/community-writers/) -to get paid for writing complete articles for GitLab. +### Mobile development + +For help with using GitLab CI/CD for mobile application development, see: -[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates +- [How to publish Android apps to the Google Play Store with GitLab and fastlane](https://about.gitlab.com/2019/01/28/android-publishing-with-gitlab-and-fastlane/) +- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/) +- [Working with YAML in GitLab CI from the Android perspective](https://about.gitlab.com/2017/11/20/working-with-yaml-gitlab-ci-android/) +- [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) +- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index 9e657275d50..589912e7a2a 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -2,7 +2,7 @@ redirect_from: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html' author: Fabio Busatto author_gitlab: bikebilly -level: intermediary +level: intermediate article_type: tutorial date: 2017-08-15 --- @@ -11,7 +11,7 @@ date: 2017-08-15 ## Introduction -In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) +In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/product/continuous-integration/) to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency. You'll create two different projects: @@ -19,7 +19,7 @@ You'll create two different projects: - `simple-maven-dep`: the app built and deployed to Artifactory (available at <https://gitlab.com/gitlab-examples/maven/simple-maven-dep>) - `simple-maven-app`: the app using the previous one as a dependency (available at <https://gitlab.com/gitlab-examples/maven/simple-maven-app>) -We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/). +We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/product/continuous-integration/). We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. ## Create the simple Maven dependency @@ -102,7 +102,7 @@ parameter in `.gitlab-ci.yml` to use the custom location instead of the default ### Configure GitLab CI/CD for `simple-maven-dep` -Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency! +Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/product/continuous-integration/) to automatically build, test and deploy the dependency! GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). @@ -230,7 +230,7 @@ Now you are ready to use the Artifactory repository to resolve dependencies and You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`. -You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application, +You want to leverage [GitLab CI/CD](https://about.gitlab.com/product/continuous-integration/) to automatically build, test and run your awesome application, and see if you can get the greeting as expected! All you need to do is to add the following `.gitlab-ci.yml` to the repo: diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index 474a481836a..d9152c45595 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -1,7 +1,7 @@ --- author: Dylan Griffith author_gitlab: DylanGriffith -level: intermediary +level: intermediate article_type: tutorial date: 2018-06-07 description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD" diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 1e2be2e8475..04633fa9dc4 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -1,7 +1,7 @@ --- author: Ryan Hall author_gitlab: blitzgren -level: intermediary +level: intermediate article_type: tutorial date: 2018-03-07 --- @@ -386,7 +386,7 @@ Uploading artifacts to coordinator... ok id=17095874 responseStatus=2 We have our codebase built and tested on every push. To complete the full pipeline with Continuous Deployment, let's set up [free web hosting with AWS S3](https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/) and a job through which our build artifacts get -deployed. GitLab also has a free static site hosting service we could use, [GitLab Pages](https://about.gitlab.com/features/pages/), +deployed. GitLab also has a free static site hosting service we could use, [GitLab Pages](https://about.gitlab.com/product/pages/), however Dark Nova specifically uses other AWS tools that necessitates using `AWS S3`. Read through this article that describes [deploying to both S3 and GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) and further delves into the principles of GitLab CI/CD than discussed in this article. diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index d3b6650b0f4..3963a3e511d 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -2,7 +2,7 @@ redirect_from: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html' author: Mehran Rasulian author_gitlab: mehranrasulian -level: intermediary +level: intermediate article_type: tutorial date: 2017-08-31 --- diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index 61bf68fa0e8..99a4316ab0d 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -1,4 +1,4 @@ -# Test and Deploy a python application with GitLab CI/CD +# Test and deploy a Python application with GitLab CI/CD This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application. @@ -65,7 +65,7 @@ First install [Docker Engine](https://docs.docker.com/installation/). To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner). You can use public runners available on `gitlab.com`, but you can register your own: -``` +```sh gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 46e6efccaf8..3a0ddf001b8 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -1,4 +1,4 @@ -# Test and Deploy a ruby application with GitLab CI/CD +# Test and deploy a Ruby application with GitLab CI/CD This example will guide you how to run tests in your Ruby on Rails application and deploy it automatically as Heroku application. @@ -58,10 +58,10 @@ You can do this through the [Dashboard](https://dashboard.heroku.com/). ## Create Runner First install [Docker Engine](https://docs.docker.com/installation/). -To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). +To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner/). You can use public runners available on `gitlab.com`, but you can register your own: -``` +```sh gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md index 66bfa41cad9..24328bf6c02 100644 --- a/doc/ci/examples/test-scala-application.md +++ b/doc/ci/examples/test-scala-application.md @@ -1,4 +1,4 @@ -# Test and deploy to Heroku a Scala application +# Test and deploy a Scala application to Heroku This example demonstrates the integration of GitLab CI with Scala applications using SBT. Checkout the example diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md index eeb89f80e09..317e2009c26 100644 --- a/doc/ci/introduction/index.md +++ b/doc/ci/introduction/index.md @@ -78,10 +78,10 @@ scripts to be specified in a file called [`.gitlab-ci.yml`](../yaml/README.md), located in the root path of your repository. In this file, you can define the scripts you want to run, define include and -cache dependencies, choose what commands you want to run in sequence +cache dependencies, choose commands you want to run in sequence and those you want to run in parallel, define where you want to -deploy your app, and choose if you want to run the script automatically -or if you want to trigger it manually. Once you're familiar with +deploy your app, and specify whether you will want to run the scripts automatically +or trigger any of them manually. Once you're familiar with GitLab CI/CD you can add more advanced steps into the configuration file. To add scripts to that file, you'll need to organize them in a @@ -175,7 +175,7 @@ file, so we recommend you read through it to understand GitLab's CI/CD logic, and learn how to write your own script (or tweak an existing one) for any application. -For an deep view of GitLab's CI/CD configuration options, check the +For a deep view of GitLab's CI/CD configuration options, check the [`.gitlab-ci.yml` full reference](../yaml/README.md). ### GitLab CI/CD feature set diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 12b2df65fdd..985895acce3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1,26 +1,39 @@ -# Configuration of your pipelines with .gitlab-ci.yml +# GitLab CI/CD Pipeline Configuration Reference -This document describes the usage of `.gitlab-ci.yml`, the file that is used by -GitLab Runner to manage your project's pipelines. +GitLab CI/CD [pipelines](../pipelines.md) are configured using a YAML file called `.gitlab-ci.yml` within each project. -From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) -file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root -of your repository and contains definitions of how your project should be built. +The `.gitlab-ci.yml` file defines the structure and order of the pipelines and determines: -If you want a quick introduction to GitLab CI, follow our -[quick start guide](../quick_start/README.md). +- What to execute using [GitLab Runner](https://docs.gitlab.com/runner/). +- What decisions to make when specific conditions are encountered. For example, when a process succeeds or fails. + +This topic covers CI/CD pipeline configuration. For other CI/CD configuration information, see: + +- [GitLab CI/CD Variables](../variables/README.md), for configuring the environment the pipelines run in. +- [GitLab Runner advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html), for configuring GitLab Runner. + +We have complete examples of configuring pipelines: + +- For a quick introduction to GitLab CI, follow our [quick start guide](../quick_start/README.md). +- For a collection of examples, see [GitLab CI/CD Examples](../examples/README.md). +- To see a large `.gitlab-ci.yml` file used in an enterprise, see the [`.gitlab-ci.yml` file for `gitlab-ce`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml). NOTE: **Note:** If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter), you may need to enable pipeline triggering in your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**. -## Jobs +## Introduction + +Pipeline configuration begins with jobs. Jobs are the most fundamental element of a `.gitlab-ci.yml` file. -The YAML file defines a set of jobs with constraints stating when they should -be run. You can specify an unlimited number of jobs which are defined as -top-level elements with an arbitrary name and always have to contain at least -the `script` clause. +Jobs are: + +- Defined with constraints stating under what conditions they should be executed. +- Top-level elements with an arbitrary name and must contain at least the [`script`](#script) clause. +- Not limited in how many can be defined. + +For example: ```yaml job1: @@ -39,6 +52,14 @@ Jobs are picked up by [Runners](../runners/README.md) and executed within the environment of the Runner. What is important, is that each job is run independently from each other. +### Validate the .gitlab-ci.yml + +Each instance of GitLab CI has an embedded debug tool called Lint, which validates the +content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your +project namespace. For example, `http://gitlab.example.com/gitlab-org/project-123/-/ci/lint`. + +### Unavailable names for jobs + Each job must have a unique name, but there are a few **reserved `keywords` that cannot be used as job names**: @@ -51,42 +72,137 @@ cannot be used as job names**: - `variables` - `cache` -A job is defined by a list of parameters that define the job behavior. - -| Keyword | Required | Description | -|---------------|----------|-------------| -| [script](#script) | yes | Defines a shell script which is executed by Runner | -| [extends](#extends) | no | Defines a configuration entry that this job is going to inherit from | -| [include](#include) | no | Defines a configuration entry that allows this job to include external YAML files | -| [image](#image-and-services) | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | -| [services](#image-and-services) | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | -| [stage](#stage) | no | Defines a job stage (default: `test`) | -| type | no | Alias for `stage` | -| [variables](#variables) | no | Define job variables on a job level | -| [only](#only-and-except-simplified) | no | Defines a list of git refs for which job is created | -| [except](#only-and-except-simplified) | no | Defines a list of git refs for which job is not created | -| [tags](#tags) | no | Defines a list of tags which are used to select Runner | -| [allow_failure](#allow_failure) | no | Allow job to fail. Failed job doesn't contribute to commit status | -| [when](#when) | no | Define when to run job. Can be `on_success`, `on_failure`, `always` or `manual` | -| [dependencies](#dependencies) | no | Define other jobs that a job depends on so that you can pass artifacts between them| -| [artifacts](#artifacts) | no | Define list of [job artifacts](#artifacts) | -| [cache](#cache) | no | Define list of files that should be cached between subsequent runs | -| [before_script](#before_script-and-after_script) | no | Override a set of commands that are executed before job | -| [after_script](#before_script-and-after_script) | no | Override a set of commands that are executed after job | -| [environment](#environment) | no | Defines a name of environment to which deployment is done by this job | -| [coverage](#coverage) | no | Define code coverage settings for a given job | -| [retry](#retry) | no | Define when and how many times a job can be auto-retried in case of a failure | -| [parallel](#parallel) | no | Defines how many instances of a job should be run in parallel | - -## `image` and `services` - -This allows to specify a custom Docker image and a list of services that can be -used for time of the job. The configuration of this feature is covered in -[a separate document](../docker/README.md). - -## `before_script` and `after_script` - -> Introduced in GitLab 8.7 and requires GitLab Runner v1.2 +### Using reserved keywords + +If you get validation error when using specific values (for example, `true` or `false`), try to: + +- Quote them. +- Change them to a different form. For example, `/bin/true`. + +## Configuration parameters + +A job is defined as a list of parameters that define the job's behavior. + +The following table lists available parameters for jobs: + +| Keyword | Description | +|:---------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`script`](#script) | Shell script which is executed by Runner. | +| [`image`](#image) | Use docker images. Also available: `image:name` and `image:entrypoint`. | +| [`services`](#services) | Use docker services images. Also available: `services:name`, `services:alias`, `services:entrypoint`, and `services:command`. | +| [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. | +| [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. | +| [`stages`](#stages) | Define stages in a pipeline. | +| [`stage`](#stage) | Defines a job stage (default: `test`). | +| [`only`](#onlyexcept-basic) | Limit when jobs are created. Also available: [`only:refs`, `only:kubernetes`, `only:variables`, and `only:changes`](#onlyexcept-advanced). | +| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). | +| [`tags`](#tags) | List of tags which are used to select Runner. | +| [`allow_failure`](#allow_failure) | Allow job to fail. Failed job doesn't contribute to commit status. | +| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. | +| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, and `environment:action`. | +| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. | +| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, and `artifacts:reports:junit`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_management`, and `artifacts:reports:performance`. | +| [`dependencies`](#dependencies) | Other jobs that a job depends on so that you can pass artifacts between them. | +| [`coverage`](#coverage) | Code coverage settings for a given job. | +| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. | +| [`parallel`](#parallel) | How many instances of a job should be run in parallel. | +| [`trigger`](#trigger-premium) | Defines a downstream pipeline trigger. | +| [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. | +| [`extends`](#extends) | Configuration entry that this job is going to inherit from. | +| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. | +| [`variables`](#variables) | Define job variables on a job level. | + +NOTE: **Note:** +Parameters `types` and `type` are [deprecated](#deprecated-parameters). + +## Parameter details + +The following are detailed explanations for parameters used to configure CI/CD pipelines. + +### `script` + +`script` is the only required keyword that a job needs. It's a shell script +which is executed by the Runner. For example: + +```yaml +job: + script: "bundle exec rspec" +``` + +This parameter can also contain several commands using an array: + +```yaml +job: + script: + - uname -a + - bundle exec rspec +``` + +NOTE: **Note:** +Sometimes, `script` commands will need to be wrapped in single or double quotes. +For example, commands that contain a colon (`:`) need to be wrapped in quotes so +that the YAML parser knows to interpret the whole thing as a string rather than +a "key: value" pair. Be careful when using special characters: +`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``. + +### `image` + +Used to specify [a Docker image](../docker/using_docker_images.md#what-is-an-image) to use for the job. + +For: + +- Simple definition examples, see [Define `image` and `services` from .gitlab-ci.yml](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml). +- Detailed usage information, refer to [Docker integration](../docker/README.md) documentation. + +#### `image:name` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see [Available settings for `image`](../docker/using_docker_images.md#available-settings-for-image). + +#### `image:entrypoint` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see [Available settings for `image`](../docker/using_docker_images.md#available-settings-for-image). + +### `services` + +Used to specify a [service Docker image](../docker/using_docker_images.md#what-is-a-service), linked to a base image specified in [`image`](#image). + +For: + +- Simple definition examples, see [Define `image` and `services` from .gitlab-ci.yml](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml). +- Detailed usage information, refer to [Docker integration](../docker/README.md) documentation. +- For example services, see [GitLab CI Services](../services/README.md). + +#### `services:name` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services). + +#### `services:alias` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services). + +#### `services:entrypoint` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services). + +#### `services:command` + +An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options). + +For more information, see see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services). + +### `before_script` and `after_script` + +> Introduced in GitLab 8.7 and requires GitLab Runner v1.2. `before_script` is used to define the command that should be run before all jobs, including deploy jobs, but after the restoration of [artifacts](#artifacts). @@ -116,7 +232,7 @@ job: - execute this after my script ``` -## `stages` +### `stages` `stages` is used to define stages that can be used by jobs and is defined globally. @@ -150,7 +266,7 @@ There are also two edge cases worth mentioning: `test` and `deploy` are allowed to be used as job's stage by default. 1. If a job doesn't specify a `stage`, the job is assigned the `test` stage. -## `stage` +### `stage` `stage` is defined per-job and relies on [`stages`](#stages) which is defined globally. It allows to group jobs into different stages, and jobs of the same @@ -179,38 +295,7 @@ job 4: script: make deploy ``` -## `types` - -CAUTION: **Deprecated:** -`types` is deprecated, and could be removed in one of the future releases. -Use [stages](#stages) instead. - -## `script` - -`script` is the only required keyword that a job needs. It's a shell script -which is executed by the Runner. For example: - -```yaml -job: - script: "bundle exec rspec" -``` - -This parameter can also contain several commands using an array: - -```yaml -job: - script: - - uname -a - - bundle exec rspec -``` - -Sometimes, `script` commands will need to be wrapped in single or double quotes. -For example, commands that contain a colon (`:`) need to be wrapped in quotes so -that the YAML parser knows to interpret the whole thing as a string rather than -a "key: value" pair. Be careful when using special characters: -`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``. - -## `only` and `except` (simplified) +### `only`/`except` (basic) `only` and `except` are two parameters that set a job policy to limit when jobs are created: @@ -299,7 +384,7 @@ job: only: ['branches', 'tags'] ``` -## `only` and `except` (complex) +### `only`/`except` (advanced) > - `refs` and `kubernetes` policies introduced in GitLab 10.0. > - `variables` policy introduced in GitLab 10.7. @@ -323,7 +408,7 @@ If you use multiple keys under `only` or `except`, they act as an AND. The logic > (any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active) -### `only:refs` and `except:refs` +#### `only:refs`/`except:refs` The `refs` strategy can take the same values as the [simplified only/except configuration](#only-and-except-simplified). @@ -339,7 +424,7 @@ deploy: - schedules ``` -### `only:kubernetes` and `except:kubernetes` +#### `only:kubernetes`/`except:kubernetes` The `kubernetes` strategy accepts only the `active` keyword. @@ -352,7 +437,7 @@ deploy: kubernetes: active ``` -### `only:variables` and `except:variables` +#### `only:variables`/`except:variables` The `variables` keyword is used to define variables expressions. In other words, you can use predefined variables / project / group or @@ -384,7 +469,7 @@ end-to-end: Learn more about [variables expressions](../variables/README.md#variables-expressions). -### `only:changes` and `except:changes` +#### `only:changes`/`except:changes` Using the `changes` keyword with `only` or `except`, makes it possible to define if a job should be created based on files modified by a git push event. @@ -415,7 +500,7 @@ CAUTION: **Warning:** There are some caveats when using this feature with new branches and tags. See the section below. -#### Using `changes` with new branches and tags +##### Using `changes` with new branches and tags If you are pushing a **new** branch or a **new** tag to GitLab, the policy always evaluates to true and GitLab will create a job. This feature is not @@ -423,7 +508,7 @@ connected with merge requests yet, and because GitLab is creating pipelines before an user can create a merge request we don't know a target branch at this point. -#### Using `changes` with `merge_requests` +##### Using `changes` with `merge_requests` With [pipelines for merge requests](../merge_request_pipelines/index.md), make it possible to define if a job should be created base on files modified @@ -446,7 +531,7 @@ In the scenario above, if you create or update a merge request that changes either files in `service-one` folder or `Dockerfile`, GitLab creates and triggers the `docker build service one` job. -## `tags` +### `tags` `tags` is used to select specific Runners from the list of all Runners that are allowed to run this project. @@ -489,7 +574,7 @@ osx job: - echo "Hello, $USER!" ``` -## `allow_failure` +### `allow_failure` `allow_failure` allows a job to fail without impacting the rest of the CI suite. @@ -525,7 +610,7 @@ job3: - deploy_to_staging ``` -## `when` +### `when` `when` is used to implement jobs that are run in case of failure or despite the failure. @@ -587,10 +672,8 @@ The above script will: success or failure. 1. Allow you to manually execute `deploy_job` from GitLab's UI. -### `when:manual` +#### `when:manual` -> **Notes:** -> > - Introduced in GitLab 8.10. > - Blocking manual actions were introduced in GitLab 9.0. > - Protected actions were introduced in GitLab 9.2. @@ -622,7 +705,7 @@ a user wants to trigger an action. In other words, in order to trigger a manual action assigned to a branch that the pipeline is running for, the user needs to have the ability to merge to this branch. -### `when:delayed` +#### `when:delayed` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4. @@ -658,10 +741,8 @@ This job will never be executed in the future unless you execute the job manuall You can start a delayed job immediately by clicking the **Play** button. GitLab runner will pick your job soon and start the job. -## `environment` +### `environment` -> **Notes:** -> > - Introduced in GitLab 8.9. > - You can read more about environments and find more examples in the > [documentation about environments][environment]. @@ -683,10 +764,8 @@ deploy to production: In the above example, the `deploy to production` job will be marked as doing a deployment to the `production` environment. -### `environment:name` +#### `environment:name` -> **Notes:** -> > - Introduced in GitLab 8.11. > - Before GitLab 8.11, the name of an environment could be defined as a string like > `environment: production`. The recommended way now is to define it under the @@ -722,10 +801,8 @@ deploy to production: name: production ``` -### `environment:url` +#### `environment:url` -> **Notes:** -> > - Introduced in GitLab 8.11. > - Before GitLab 8.11, the URL could be added only in GitLab's UI. The > recommended way now is to define it in `.gitlab-ci.yml`. @@ -749,10 +826,8 @@ deploy to production: url: https://prod.example.com ``` -### `environment:on_stop` +#### `environment:on_stop` -> **Notes:** -> > - [Introduced][ce-6669] in GitLab 8.13. > - Starting with GitLab 8.14, when you have an environment that has a stop action > defined, GitLab will automatically trigger a stop action when the associated @@ -764,7 +839,7 @@ the environment. Read the `environment:action` section for an example. -### `environment:action` +#### `environment:action` > [Introduced][ce-6669] in GitLab 8.13. @@ -805,10 +880,8 @@ The `stop_review_app` job is **required** to have the following keywords defined - `stage` should be the same as the `review_app` in order for the environment to stop automatically when the branch is deleted -### Dynamic environments +#### Dynamic environments -> **Notes:** -> > - [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. > - The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15. > - The `name` and `url` parameters can use any of the defined CI variables, @@ -841,10 +914,8 @@ The common use case is to create dynamic environments for branches and use them as Review Apps. You can see a simple example using Review Apps at <https://gitlab.com/gitlab-examples/review-apps-nginx/>. -## `cache` +### `cache` -> **Notes:** -> > - Introduced in GitLab Runner v0.7.0. > - `cache` can be set globally and per-job. > - From GitLab 9.0, caching is enabled and shared between pipelines and jobs @@ -862,7 +933,7 @@ workspace. If `cache` is defined outside the scope of jobs, it means it is set globally and all jobs will use that definition. -### `cache:paths` +#### `cache:paths` Use the `paths` directive to choose which files or directories will be cached. Wildcards can be used as well. @@ -898,7 +969,7 @@ Note that since cache is shared between jobs, if you're using different paths for different jobs, you should also set a different **cache:key** otherwise cache content can be overwritten. -### `cache:key` +#### `cache:key` > Introduced in GitLab Runner v1.0.0. @@ -939,7 +1010,7 @@ cache: - binaries/ ``` -### `cache:untracked` +#### `cache:untracked` Set `untracked: true` to cache all files that are untracked in your Git repository: @@ -962,7 +1033,7 @@ rspec: - binaries/ ``` -### `cache:policy` +#### `cache:policy` > Introduced in GitLab 9.4. @@ -1009,10 +1080,8 @@ Additionally, if you have a job that unconditionally recreates the cache without reference to its previous contents, you can use `policy: push` in that job to skip the download step. -## `artifacts` +### `artifacts` -> **Notes:** -> > - Introduced in GitLab Runner v0.7.0 for non-Windows platforms. > - Windows support was added in GitLab Runner v.1.0.0. > - From GitLab 9.2, caches are restored before artifacts. @@ -1025,9 +1094,9 @@ attached to the job after success. The artifacts will be sent to GitLab after the job finishes successfully and will be available for download in the GitLab UI. -[Read more about artifacts.](../../user/project/pipelines/job_artifacts.md) +[Read more about artifacts](../../user/project/pipelines/job_artifacts.md). -### `artifacts:paths` +#### `artifacts:paths` You can only use paths that are within the project workspace. To pass artifacts between different jobs, see [dependencies](#dependencies). @@ -1072,7 +1141,7 @@ release-job: - tags ``` -### `artifacts:name` +#### `artifacts:name` > Introduced in GitLab 8.6 and GitLab Runner v1.1.0. @@ -1153,7 +1222,7 @@ job: - binaries/ ``` -### `artifacts:untracked` +#### `artifacts:untracked` `artifacts:untracked` is used to add all Git untracked files as artifacts (along to the paths defined in `artifacts:paths`). @@ -1178,7 +1247,7 @@ artifacts: - binaries/ ``` -### `artifacts:when` +#### `artifacts:when` > Introduced in GitLab 8.9 and GitLab Runner v1.3.0. @@ -1199,7 +1268,7 @@ job: when: on_failure ``` -### `artifacts:expire_in` +#### `artifacts:expire_in` > Introduced in GitLab 8.9 and GitLab Runner v1.3.0. @@ -1234,7 +1303,7 @@ job: expire_in: 1 week ``` -### `artifacts:reports` +#### `artifacts:reports` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in GitLab 11.2. Requires GitLab Runner 11.2 and above. @@ -1252,7 +1321,7 @@ NOTE: **Note:** If you also want the ability to browse the report output files, include the [`artifacts:paths`](#artifactspaths) keyword. -#### `artifacts:reports:junit` +##### `artifacts:reports:junit` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in GitLab 11.2. Requires GitLab Runner 11.2 and above. @@ -1286,7 +1355,7 @@ concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`), an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`). -#### `artifacts:reports:codequality` **[STARTER]** +##### `artifacts:reports:codequality` **[STARTER]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1296,7 +1365,7 @@ as artifacts. The collected Code Quality report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests. -#### `artifacts:reports:sast` **[ULTIMATE]** +##### `artifacts:reports:sast` **[ULTIMATE]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1307,7 +1376,7 @@ The collected SAST report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests, pipeline view and provide data for security dashboards. -#### `artifacts:reports:dependency_scanning` **[ULTIMATE]** +##### `artifacts:reports:dependency_scanning` **[ULTIMATE]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1318,7 +1387,7 @@ The collected Dependency Scanning report will be uploaded to GitLab as an artifa be automatically shown in merge requests, pipeline view and provide data for security dashboards. -#### `artifacts:reports:container_scanning` **[ULTIMATE]** +##### `artifacts:reports:container_scanning` **[ULTIMATE]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1329,7 +1398,7 @@ The collected Container Scanning report will be uploaded to GitLab as an artifac be automatically shown in merge requests, pipeline view and provide data for security dashboards. -#### `artifacts:reports:dast` **[ULTIMATE]** +##### `artifacts:reports:dast` **[ULTIMATE]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1340,7 +1409,7 @@ The collected DAST report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests, pipeline view and provide data for security dashboards. -#### `artifacts:reports:license_management` **[ULTIMATE]** +##### `artifacts:reports:license_management` **[ULTIMATE]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1351,7 +1420,7 @@ The collected License Management report will be uploaded to GitLab as an artifac be automatically shown in merge requests, pipeline view and provide data for security dashboards. -#### `artifacts:reports:performance` **[PREMIUM]** +##### `artifacts:reports:performance` **[PREMIUM]** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. @@ -1361,7 +1430,7 @@ as artifacts. The collected Performance report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests. -## `dependencies` +### `dependencies` > Introduced in GitLab 8.6 and GitLab Runner v1.1.1. @@ -1420,7 +1489,7 @@ deploy: script: make deploy ``` -### When a dependent job will fail +#### When a dependent job will fail > Introduced in GitLab 10.3. @@ -1434,7 +1503,7 @@ You can ask your administrator to [flip this switch](../../administration/job_artifacts.md#validation-for-dependencies) and bring back the old behavior. -## `coverage` +### `coverage` > [Introduced][ce-7447] in GitLab 8.17. @@ -1454,7 +1523,7 @@ job1: coverage: '/Code coverage: \d+\.\d+/' ``` -## `retry` +### `retry` > [Introduced][ce-12909] in GitLab 9.5. > [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21758) @@ -1528,7 +1597,7 @@ Possible values for `when` are: - `missing_dependency_failure`: Retry if a dependency was missing. - `runner_unsupported`: Retry if the runner was unsupported. -## `parallel` +### `parallel` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. @@ -1548,7 +1617,49 @@ test: parallel: 5 ``` -## `include` +### `trigger` **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8. + +`trigger` allows you to define downstream pipeline trigger. When a job created +from `trigger` definition is started by GitLab, a downstream pipeline gets +created. + +Learn more about [multi-project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipelines.html#creating-cross-project-pipelines-from-gitlab-ci-yml). + +#### Simple `trigger` syntax + +The most simple way to configure a downstream trigger to use `trigger` keyword +with a full path to a downstream project: + +```yaml +rspec: + stage: test + script: bundle exec rspec + +staging: + stage: deploy + trigger: my/deployment +``` + +#### Complex `trigger` syntax + +It is possible to configure a branch name that GitLab will use to create +a downstream pipeline with: + +```yaml +rspec: + stage: test + script: bundle exec rspec + +staging: + stage: deploy + trigger: + project: my/deployment + branch: stable +``` + +### `include` > - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. > - Available for Starter, Premium and Ultimate since 10.6. @@ -1585,7 +1696,7 @@ of using YAML anchors, you can use the [`extends` keyword](#extends). See [usage examples](#include-examples). -### `include:local` +#### `include:local` `include:local` includes a file from the same repository as `.gitlab-ci.yml`. It's referenced using full paths relative to the root directory (`/`). @@ -1607,7 +1718,7 @@ include: - local: '/templates/.gitlab-ci-template.yml' ``` -### `include:file` +#### `include:file` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7. @@ -1638,11 +1749,11 @@ include: file: '/templates/.gitlab-ci-template.yml' ``` -All nested includes will be executed in the scope of the target project, +All [nested includes](#nested-includes) will be executed in the scope of the target project, so it is possible to used local (relative to target project), project, remote or template includes. -### `include:template` +#### `include:template` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) in GitLab 11.7. @@ -1657,10 +1768,10 @@ include: - template: Auto-DevOps.gitlab-ci.yml ``` -All nested includes will be executed only with the permission of the user, +All [nested includes](#nested-includes) will be executed only with the permission of the user, so it is possible to use project, remote or template includes. -### `include:remote` +#### `include:remote` `include:remote` can be used to include a file from a different location, using HTTP/HTTPS, referenced by using the full URL. The remote file must be @@ -1675,7 +1786,7 @@ include: All nested includes will be executed without context as public user, so only another remote, or public project, or template is allowed. -### Nested includes +#### Nested includes > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7. @@ -1683,11 +1794,11 @@ Nested includes allow you to compose a set of includes. A total of 50 includes is allowed. Duplicate includes are considered a configuration error. -### `include` examples +#### `include` examples Here are a few more `include` examples. -#### Single string or array of multiple values +##### Single string or array of multiple values You can include your extra YAML file(s) either as a single string or an array of multiple values. The following examples are all valid. @@ -1741,7 +1852,7 @@ include: file: '/templates/.gitlab-ci-template.yml' ``` -#### Re-using a `before_script` template +##### Re-using a `before_script` template In the following example, the content of `.before-script-template.yml` will be automatically fetched and evaluated along with the content of `.gitlab-ci.yml`. @@ -1765,7 +1876,7 @@ rspec: - bundle exec rspec ``` -#### Overriding external template values +##### Overriding external template values The following example shows specific YAML-defined variables and details of the `production` job from an include file being customized in `.gitlab-ci.yml`. @@ -1850,7 +1961,7 @@ In this case, if `install_dependencies` and `deploy` were not repeated in `.gitlab-ci.yml`, they would not be part of the script for the `production` job in the combined CI configuration. -#### Using nested includes +##### Using nested includes The examples below show how includes can be nested from different sources using a combination of different methods. @@ -1895,7 +2006,7 @@ docker-test: script: docker run my-image /run/tests.sh ``` -## `extends` +### `extends` > Introduced in GitLab 11.3. @@ -1976,7 +2087,7 @@ spinach: script: rake spinach ``` -## Using `extends` and `include` together +### Using `extends` and `include` together `extends` works across configuration files combined with `include`. @@ -2001,7 +2112,7 @@ useTemplate: This will run a job called `useTemplate` that runs `echo Hello!` as defined in the `.template` job, and uses the `alpine` Docker image as defined in the local job. -## `pages` +### `pages` `pages` is a special job that is used to upload static content to GitLab that can be used to serve your website. It has a special syntax, so the two @@ -2030,7 +2141,7 @@ pages: Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). -## `variables` +### `variables` > Introduced in GitLab Runner v0.5.0. @@ -2063,9 +2174,9 @@ you can set in `.gitlab-ci.yml`, there are also the so called [Variables](../variables/README.md#variables) which can be set in GitLab's UI. -[Learn more about variables and their priority.][variables] +Learn more about [variables and their priority][variables]. -### Git strategy +#### Git strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed > completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner @@ -2110,7 +2221,7 @@ NOTE: **Note:** `GIT_STRATEGY` is not supported for but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847) for updates. -### Git submodule strategy +#### Git submodule strategy > Requires GitLab Runner v1.10+. @@ -2148,9 +2259,9 @@ Note that for this feature to work correctly, the submodules must be configured - a relative path to another repository on the same GitLab server. See the [Git submodules](../git_submodules.md) documentation. -### Git checkout +#### Git checkout -> Introduced in GitLab Runner 9.3 +> Introduced in GitLab Runner 9.3. The `GIT_CHECKOUT` variable can be used when the `GIT_STRATEGY` is set to either `clone` or `fetch` to specify whether a `git checkout` should be run. If not @@ -2177,7 +2288,7 @@ script: - git merge $CI_BUILD_REF_NAME ``` -### Job stages attempts +#### Job stages attempts > Introduced in GitLab, it requires GitLab Runner v1.9+. @@ -2201,7 +2312,7 @@ variables: You can set them globally or per-job in the [`variables`](#variables) section. -### Shallow cloning +#### Shallow cloning > Introduced in GitLab 8.9 as an experimental feature. May change in future releases or be removed completely. @@ -2211,7 +2322,7 @@ shallow cloning of the repository which can significantly speed up cloning for repositories with a large number of commits or old, large binaries. The value is passed to `git fetch` and `git clone`. ->**Note:** +NOTE: **Note:** If you use a depth of 1 and have a queue of jobs or retry jobs, jobs may fail. @@ -2234,6 +2345,22 @@ variables: You can set it globally or per-job in the [`variables`](#variables) section. +## Deprecated parameters + +The following parameters are deprecated. + +### `types` + +CAUTION: **Deprecated:** +`types` is deprecated, and could be removed in a future release. +Use [`stages`](#stages) instead. + +### `type` + +CAUTION: **Deprecated:** +`type` is deprecated, and could be removed in one of the future releases. +Use [`stage`](#stage) instead. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) @@ -2393,7 +2520,9 @@ You can see that the hidden keys are conveniently used as templates. ## Triggers Triggers can be used to force a rebuild of a specific branch, tag or commit, -with an API call. +with an API call when a pipeline gets created using a trigger token. + +Not to be confused with [`trigger`](#trigger-premium). [Read more in the triggers documentation.](../triggers/README.md) @@ -2409,22 +2538,6 @@ using Git 2.10 or newer: git push -o ci.skip ``` -## Validate the .gitlab-ci.yml - -Each instance of GitLab CI has an embedded debug tool called Lint, which validates the -content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your -project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/-/ci/lint`) - -## Using reserved keywords - -If you get validation error when using specific values (e.g., `true` or `false`), -try to quote them, or change them to a different form (e.g., `/bin/true`). - -## Examples - -See a [list of examples](../examples/README.md "CI/CD examples") for using -GitLab CI/CD with various languages. - [ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 [ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 90936034fea..a15e1a59921 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -276,7 +276,7 @@ edit existing comments. Non-team members are restricted from adding or editing c | :-----------: | :----------: | |  |  | -Additionally locked issues can not be reopened. +Additionally, locked issues and merge requests can not be reopened. ## Filtering notes diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 3819dc308ec..ef85b2f6837 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -387,27 +387,27 @@ Upgrades will reset values back to the values built into the `runner` chart plus the values set by [`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml) -## Getting the external IP address +## Getting the external endpoint NOTE: **Note:** With the following procedure, a load balancer must be installed in your cluster -to obtain the external IP address. You can use either +to obtain the endpoint. You can use either [Ingress](#installing-applications), or Knative's own load balancer ([Istio](https://istio.io)) if using [Knative](#installing-applications). -In order to publish your web application, you first need to find the external IP -address associated to your load balancer. +In order to publish your web application, you first need to find the endpoint which will be either an IP +address or a hostname associated with your load balancer. -### Let GitLab fetch the IP address +### Let GitLab fetch the external endpoint > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6. If you [installed Ingress or Knative](#installing-applications), -you should see the Ingress IP address on this same page within a few minutes. -If you don't see this, GitLab might not be able to determine the IP address of +you should see the Ingress Endpoint on this same page within a few minutes. +If you don't see this, GitLab might not be able to determine the external endpoint of your ingress application in which case you should manually determine it. -### Manually determining the IP address +### Manually determining the external endpoint If the cluster is on GKE, click the **Google Kubernetes Engine** link in the **Advanced settings**, or go directly to the @@ -417,7 +417,7 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**. If the cluster is not on GKE, follow the specific instructions for your Kubernetes provider to configure `kubectl` with the right credentials. -The output of the following examples will show the external IP address of your +The output of the following examples will show the external endpoint of your cluster. This information can then be used to set up DNS entries and forwarding rules that allow external access to your deployed applications. @@ -425,19 +425,19 @@ If you installed the Ingress [via the **Applications**](#installing-applications run the following command: ```bash -kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' +kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` -For Istio/Knative, the command will be different: +Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: ```bash -kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' +kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' ``` -Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: +For Istio/Knative, the command will be different: ```bash -kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}". +kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' ``` Otherwise, you can list the IP addresses of all load balancers: @@ -456,13 +456,12 @@ reserved IP. Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip). -### Pointing your DNS at the cluster IP +### Pointing your DNS at the external endpoint -Once you've set up the static IP, you should associate it to a [wildcard DNS -record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able -to reach your apps. This heavily depends on your domain provider, but in case -you aren't sure, just create an A record with a wildcard host like -`*.example.com.`. +Once you've set up the external endpoint, you should associate it with a [wildcard DNS +record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.` +in order to be able to reach your apps. If your external endpoint is an IP address, +use an A record. If your external endpoint is a hostname, use a CNAME record. ## Multiple Kubernetes clusters **[PREMIUM]** diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 856ae03f4bc..e6804666e22 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -37,9 +37,9 @@ To run Knative on Gitlab, you will need: applications or functions onto your cluster. You can install the GitLab Runner onto the existing Kubernetes cluster. See [Installing Applications](../index.md#installing-applications) for more information. 1. **Domain Name:** Knative will provide its own load balancer using Istio. It will provide an - external IP address for all the applications served by Knative. You will be prompted to enter a + external IP address or hostname for all the applications served by Knative. You will be prompted to enter a wildcard domain where your applications will be served. Configure your DNS server to use the - external IP address for that domain. + external IP address or hostname for that domain. 1. **`.gitlab-ci.yml`:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko) to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm) to simplify the deployment of knative services and functions. @@ -62,18 +62,8 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.  -1. After the Knative installation has finished, you can wait for the IP address to be displayed in the - **Knative IP Address** field (takes up to 5 minutes) or retrieve the Istio Ingress IP address by running the following command: - - ```bash - kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' - ``` - - Output: - - ```bash - 35.161.143.124 my-machine-name:~ my-user$ - ``` +1. After the Knative installation has finished, you can wait for the IP address or hostname to be displayed in the + **Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../#manually-determining-the-external-endpoint). NOTE: **Note:** Running `kubectl` commands on your cluster requires setting up access to the cluster first. @@ -82,8 +72,8 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22. 1. The ingress is now available at this address and will route incoming requests to the proper service based on the DNS name in the request. To support this, a wildcard DNS A record should be created for the desired domain name. For example, - if your Knative base domain is `example.com` then you need to create an A record with domain `*.example.com` - pointing the ip address of the ingress. + if your Knative base domain is `knative.info` then you need to create an A record or CNAME record with domain `*.knative.info` + pointing the ip address or hostname of the ingress.  diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index b7601f26802..de7fc93f0a4 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx- ### About managed NGINX Ingress deployments -NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address). +NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint). NGINX is configured for Prometheus monitoring, by setting: diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md index 081eb8732ad..31ac53c0d14 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md @@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx- ### About managed NGINX Ingress deployments -NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address). +NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint). NGINX is configured for Prometheus monitoring, by setting: diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 1058f4e8a5e..c86eae6f2da 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -22,9 +22,22 @@ module API message: "should be an integer, 'None' or 'Any'" end end + + class ArrayNoneAny < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.is_a?(Array) || + [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase) + + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], + message: "should be an array, 'None' or 'Any'" + end + end end end end Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny) +Grape::Validations.register_validator(:array_none_any, ::API::Helpers::CustomValidators::ArrayNoneAny) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 123b7a83185..98dcc388f44 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -12,6 +12,9 @@ module API helpers do params :optional_params_ee do end + + params :optional_merge_requests_search_params do + end end def self.update_params_at_least_one_of @@ -112,6 +115,8 @@ module API optional :search, type: String, desc: 'Search merge requests for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' optional :wip, type: String, values: %w[yes no], desc: 'Search merge requests for WIP in the title' + + use :optional_merge_requests_search_params use :pagination end end diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml new file mode 100644 index 00000000000..42cb452ec99 --- /dev/null +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -0,0 +1,48 @@ +# Read more about this feature here: https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html + +container_scanning: + stage: test + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + # Defining two new variables based on GitLab's CI/CD predefined variables + # https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables + CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG + CI_APPLICATION_TAG: $CI_COMMIT_SHA + # Prior to this, you need to have the Container Registry running for your project and setup a build job + # with at least the following steps: + # + # docker build -t $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG . + # docker push $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA + # + # Container Scanning deals with Docker images only so no need to import the project's Git repository: + GIT_STRATEGY: none + allow_failure: true + services: + - docker:stable-dind + script: + - docker run -d --name db arminc/clair-db:latest + - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.1 + - apk add -U wget ca-certificates + - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} + - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 + - mv clair-scanner_linux_amd64 clair-scanner + - chmod +x clair-scanner + - touch clair-whitelist.yml + - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done + - retries=0 + - echo "Waiting for clair daemon to start" + - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done + - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true + artifacts: + reports: + container_scanning: gl-container-scanning-report.json + dependencies: [] + only: + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/ + except: + variables: + - $CONTAINER_SCANNING_DISABLED diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml new file mode 100644 index 00000000000..4e708f229cd --- /dev/null +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -0,0 +1,60 @@ +# Read more about this feature here: https://docs.gitlab.com/ee/user/project/merge_requests/dast.html + +# Configure the scanning tool through the environment variables. +# List of the variables: https://gitlab.com/gitlab-org/security-products/dast#settings +# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables + +variables: + DAST_WEBSITE: http://example.com # Please edit to be your website to scan for vulnerabilities + +stages: + - build + - test + - deploy + - dast + +dast: + stage: dast + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + before_script: + - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} + - | + function dast_run() { + docker run \ + --env DAST_TARGET_AVAILABILITY_TIMEOUT \ + --volume "$PWD:/output" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + -w /output \ + "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ + /analyze -t $DAST_WEBSITE \ + "$@" + } + script: + - | + if [ -n "$DAST_AUTH_URL" ] + then + dast_run \ + --auth-url $DAST_AUTH_URL \ + --auth-username $DAST_USERNAME \ + --auth-password $DAST_PASSWORD \ + --auth-username-field $DAST_USERNAME_FIELD \ + --auth-password-field $DAST_PASSWORD_FIELD + else + dast_run + fi + artifacts: + reports: + dast: gl-dast-report.json + only: + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bdast\b/ + except: + variables: + - $DAST_DISABLED diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index 805df26b957..fd666541d41 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -4,9 +4,6 @@ # List of the variables: https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings # How to set: https://docs.gitlab.com/ee/ci/yaml/#variables -stages: - - test - dependency_scanning: stage: test image: docker:stable diff --git a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml new file mode 100644 index 00000000000..0208beb35b8 --- /dev/null +++ b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml @@ -0,0 +1,27 @@ +# Read more about this feature here: https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html + +variables: + LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager. + +license_management: + stage: test + image: + name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" + entrypoint: [""] + variables: + SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD + allow_failure: true + script: + - /run.sh analyze . + artifacts: + reports: + license_management: gl-license-management-report.json + dependencies: [] + only: + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\blicense_management\b/ + except: + variables: + - $LICENSE_MANAGEMENT_DISABLED diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml new file mode 100644 index 00000000000..034fba5499c --- /dev/null +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -0,0 +1,43 @@ +# Read more about this feature here: https://docs.gitlab.com/ee/user/project/merge_requests/sast.html +# +# Configure the scanning tool through the environment variables. +# List of the variables: https://gitlab.com/gitlab-org/security-products/sast#settings +# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables + +sast: + stage: test + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + script: + - export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} + - | + docker run \ + --env SAST_ANALYZER_IMAGES \ + --env SAST_ANALYZER_IMAGE_PREFIX \ + --env SAST_ANALYZER_IMAGE_TAG \ + --env SAST_DEFAULT_ANALYZERS \ + --env SAST_BRAKEMAN_LEVEL \ + --env SAST_GOSEC_LEVEL \ + --env SAST_FLAWFINDER_LEVEL \ + --env SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ + --env SAST_PULL_ANALYZER_IMAGE_TIMEOUT \ + --env SAST_RUN_ANALYZER_TIMEOUT \ + --volume "$PWD:/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code + artifacts: + reports: + sast: gl-sast-report.json + dependencies: [] + only: + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bsast\b/ + except: + variables: + - $SAST_DISABLED diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index 8a908291637..99885be8755 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -30,6 +30,7 @@ module Gitlab ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift'), ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), + ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro'), ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo'), ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 05d8ac1a8d0..006ec1ec56f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -321,6 +321,9 @@ msgstr "" msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." msgstr "" +msgid "A ready-to-go template for use with Android apps." +msgstr "" + msgid "A ready-to-go template for use with iOS Swift apps." msgstr "" @@ -444,6 +447,9 @@ msgstr "" msgid "Add users to group" msgstr "" +msgid "Added at" +msgstr "" + msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission" msgstr "" @@ -609,6 +615,9 @@ msgstr "" msgid "Allow requests to the local network from hooks and services." msgstr "" +msgid "Allow this key to push to repository as well? (Default only allows pull access.)" +msgstr "" + msgid "Allow users to request access" msgstr "" @@ -1614,9 +1623,6 @@ msgstr "" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." -msgstr "" - msgid "ClusterIntegration|%{title} upgraded successfully." msgstr "" @@ -1638,9 +1644,6 @@ msgstr "" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" -msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}" -msgstr "" - msgid "ClusterIntegration|Alternatively" msgstr "" @@ -1695,7 +1698,7 @@ msgstr "" msgid "ClusterIntegration|Copy CA Certificate" msgstr "" -msgid "ClusterIntegration|Copy Ingress IP Address to clipboard" +msgid "ClusterIntegration|Copy Ingress Endpoint to clipboard" msgstr "" msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" @@ -1782,7 +1785,7 @@ msgstr "" msgid "ClusterIntegration|Ingress" msgstr "" -msgid "ClusterIntegration|Ingress IP Address" +msgid "ClusterIntegration|Ingress Endpoint" msgstr "" msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint." @@ -1797,6 +1800,12 @@ msgstr "" msgid "ClusterIntegration|Installing" msgstr "" +msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}." +msgstr "" + +msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}." +msgstr "" + msgid "ClusterIntegration|Integrate Kubernetes cluster automation" msgstr "" @@ -1878,9 +1887,6 @@ msgstr "" msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}" msgstr "" -msgid "ClusterIntegration|More information" -msgstr "" - msgid "ClusterIntegration|No machine types matched your search" msgstr "" @@ -1893,9 +1899,6 @@ msgstr "" msgid "ClusterIntegration|No zones matched your search" msgstr "" -msgid "ClusterIntegration|Note:" -msgstr "" - msgid "ClusterIntegration|Number of nodes" msgstr "" @@ -1905,7 +1908,7 @@ msgstr "" msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:" msgstr "" -msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed." +msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed." msgstr "" msgid "ClusterIntegration|Project cluster" @@ -1998,7 +2001,7 @@ msgstr "" msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain." msgstr "" -msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." +msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." msgstr "" msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below" @@ -2058,9 +2061,6 @@ msgstr "" msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|check the pricing here" -msgstr "" - msgid "ClusterIntegration|documentation" msgstr "" @@ -2070,6 +2070,9 @@ msgstr "" msgid "ClusterIntegration|meets the requirements" msgstr "" +msgid "ClusterIntegration|pricing" +msgstr "" + msgid "ClusterIntegration|properly configured" msgstr "" @@ -2932,6 +2935,9 @@ msgstr "" msgid "Edit" msgstr "" +msgid "Edit Deploy Key" +msgstr "" + msgid "Edit Label" msgstr "" @@ -2968,6 +2974,9 @@ msgstr "" msgid "Edit issues" msgstr "" +msgid "Edit public deploy key" +msgstr "" + msgid "Email" msgstr "" @@ -3534,6 +3543,9 @@ msgstr "" msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file." msgstr "" +msgid "Fingerprint" +msgstr "" + msgid "Fingerprints" msgstr "" @@ -5000,6 +5012,9 @@ msgstr "" msgid "New branch unavailable" msgstr "" +msgid "New deploy key" +msgstr "" + msgid "New directory" msgstr "" @@ -5410,6 +5425,9 @@ msgstr "" msgid "Past due" msgstr "" +msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}" +msgstr "" + msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key." msgstr "" @@ -6097,6 +6115,9 @@ msgstr "" msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group." msgstr "" +msgid "Projects with write access" +msgstr "" + msgid "ProjectsDropdown|Frequently visited" msgstr "" @@ -6208,6 +6229,9 @@ msgstr "" msgid "Public - The project can be accessed without any authentication." msgstr "" +msgid "Public deploy keys (%{deploy_keys_count})" +msgstr "" + msgid "Public pipelines" msgstr "" @@ -8666,6 +8690,9 @@ msgstr "" msgid "Write a comment or drag your files here…" msgstr "" +msgid "Write access allowed" +msgstr "" + msgid "Write milestone description..." msgstr "" @@ -8894,6 +8921,9 @@ msgstr "" msgid "a deleted user" msgstr "" +msgid "added %{created_at_timeago}" +msgstr "" + msgid "ago" msgstr "" diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index d4e1679b6bf..50a25718e96 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -11,7 +11,7 @@ module QA end view 'app/assets/javascripts/clusters/components/applications.vue' do - element :ingress_ip_address, 'id="ingress-ip-address"' # rubocop:disable QA/ElementWithPattern + element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern end view 'app/views/clusters/clusters/_form.html.haml' do @@ -35,7 +35,7 @@ module QA def ingress_ip # We need to wait longer since it can take some time before the # ip address is assigned for the ingress controller - page.find('#ingress-ip-address', wait: 1200).value + page.find('#ingress-endpoint', wait: 1200).value end def set_domain(domain) diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 4458a7223bf..d8b75c5151e 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -371,5 +371,36 @@ describe AutocompleteController do expect(json_response[3]).to match('name' => 'thumbsdown') end end + + context 'Get merge_request_target_branches' do + let(:user2) { create(:user) } + let!(:merge_request1) { create(:merge_request, source_project: project, target_branch: 'feature') } + + context 'unauthorized user' do + it 'returns empty json' do + get :merge_request_target_branches + + expect(json_response).to be_empty + end + end + + context 'sign in as user without any accesible merge requests' do + it 'returns empty json' do + sign_in(user2) + get :merge_request_target_branches + + expect(json_response).to be_empty + end + end + + context 'sign in as user with a accesible merge request' do + it 'returns json' do + sign_in(user) + get :merge_request_target_branches + + expect(json_response).to contain_exactly({ 'title' => 'feature' }) + end + end + end end end diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb new file mode 100644 index 00000000000..ffbdacc68f6 --- /dev/null +++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe 'Merge Requests > User filters by target branch', :js do + include FilteredSearchHelpers + + let!(:project) { create(:project, :public, :repository) } + let!(:user) { project.creator } + let!(:mr1) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'master') } + let!(:mr2) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'merged-target') } + + before do + sign_in(user) + visit project_merge_requests_path(project) + end + + context 'filtering by target-branch:master' do + it 'applies the filter' do + input_filtered_search('target-branch:master') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content mr1.title + expect(page).not_to have_content mr2.title + end + end + + context 'filtering by target-branch:merged-target' do + it 'applies the filter' do + input_filtered_search('target-branch:merged-target') + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).not_to have_content mr1.title + expect(page).to have_content mr2.title + end + end + + context 'filtering by target-branch:feature' do + it 'applies the filter' do + input_filtered_search('target-branch:feature') + + expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) + expect(page).not_to have_content mr1.title + expect(page).not_to have_content mr2.title + end + end +end diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 713e25cdcb2..4981bf794d9 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -82,7 +82,7 @@ describe 'Clusters Applications', :js do it 'should show info block and not be installable' do page.within('.js-cluster-application-row-knative') do - expect(page).to have_css('.bs-callout-info') + expect(page).to have_css('.rbac-notice') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') end end @@ -93,7 +93,7 @@ describe 'Clusters Applications', :js do it 'should not show callout block and be installable' do page.within('.js-cluster-application-row-knative') do - expect(page).not_to have_css('.bs-callout-info') + expect(page).not_to have_css('.rbac-notice') expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') end end @@ -226,14 +226,14 @@ describe 'Clusters Applications', :js do expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed') expect(page).to have_css('.js-cluster-application-install-button[disabled]') - expect(page).to have_selector('.js-no-ip-message') - expect(page.find('.js-ip-address').value).to eq('?') + expect(page).to have_selector('.js-no-endpoint-message') + expect(page.find('.js-endpoint').value).to eq('?') # We receive the external IP address and display Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100') - expect(page).not_to have_selector('.js-no-ip-message') - expect(page.find('.js-ip-address').value).to eq('192.168.1.100') + expect(page).not_to have_selector('.js-no-endpoint-message') + expect(page.find('.js-endpoint').value).to eq('192.168.1.100') end expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 503b88fcbad..f1178b07eec 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -36,7 +36,7 @@ describe MergeRequestsFinder do let(:project5) { create_project_without_n_plus_1(group: subgroup) } let(:project6) { create_project_without_n_plus_1(group: subgroup) } - let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } + let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 5ebc09a96dc..9da07a0b253 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -33,6 +33,7 @@ "version": { "type": "string" }, "status_reason": { "type": ["string", "null"] }, "external_ip": { "type": ["string", "null"] }, + "external_hostname": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] }, "update_available": { "type": ["boolean", "null"] } diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 8daf0282184..e2466bf326c 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -106,7 +106,7 @@ describe('Applications', () => { }, }); - expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0'); + expect(vm.$el.querySelector('.js-endpoint').value).toEqual('0.0.0.0'); expect( vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), @@ -114,6 +114,32 @@ describe('Applications', () => { }); }); + describe('with hostname', () => { + it('renders hostname with a clipboard button', () => { + vm = mountComponent(Applications, { + applications: { + ingress: { + title: 'Ingress', + status: 'installed', + externalHostname: 'localhost.localdomain', + }, + helm: { title: 'Helm Tiller' }, + cert_manager: { title: 'Cert-Manager' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, + }, + }); + + expect(vm.$el.querySelector('.js-endpoint').value).toEqual('localhost.localdomain'); + + expect( + vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), + ).toEqual('localhost.localdomain'); + }); + }); + describe('without ip address', () => { it('renders an input text with a question mark and an alert text', () => { vm = mountComponent(Applications, { @@ -126,9 +152,9 @@ describe('Applications', () => { }, }); - expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?'); + expect(vm.$el.querySelector('.js-endpoint').value).toEqual('?'); - expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null); + expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null); }); }); }); @@ -140,7 +166,7 @@ describe('Applications', () => { }); expect(vm.$el.textContent).not.toContain('Ingress IP Address'); - expect(vm.$el.querySelector('.js-ip-address')).toBe(null); + expect(vm.$el.querySelector('.js-endpoint')).toBe(null); }); }); @@ -268,11 +294,11 @@ describe('Applications', () => { it('renders ip address with a clipboard button', () => { vm = mountComponent(Applications, props); - expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('1.1.1.1'); + expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1'); expect( vm.$el - .querySelector('.js-knative-ip-clipboard-btn') + .querySelector('.js-knative-endpoint-clipboard-btn') .getAttribute('data-clipboard-text'), ).toEqual('1.1.1.1'); }); @@ -316,9 +342,9 @@ describe('Applications', () => { }, }); - expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('?'); + expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('?'); - expect(vm.$el.querySelector('.js-no-knative-ip-message')).not.toBe(null); + expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null); }); }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 3ace19c6401..b4d1bb710e0 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -17,6 +17,7 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.ERROR, status_reason: 'Cannot connect', external_ip: null, + external_hostname: null, }, { name: 'runner', @@ -62,6 +63,7 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLED, status_reason: 'Cannot connect', external_ip: '1.1.1.1', + external_hostname: null, }, { name: 'runner', diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 09bcdf91d91..161722ec571 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -78,6 +78,7 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, externalIp: null, + externalHostname: null, }, runner: { title: 'GitLab Runner', @@ -113,6 +114,7 @@ describe('Clusters Store', () => { hostname: null, isEditingHostName: false, externalIp: null, + externalHostname: null, }, cert_manager: { title: 'Cert-Manager', diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 6230da77f49..a72ea6ab547 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,9 +1,4 @@ -import _ from 'underscore'; -import AjaxCache from '~/lib/utils/ajax_cache'; -import UsersCache from '~/lib/utils/users_cache'; - import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; -import DropdownUtils from '~/filtered_search//dropdown_utils'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { @@ -298,6 +293,7 @@ describe('Filtered Search Visual Tokens', () => { subject.addVisualTokenElement('milestone'); const token = tokensContainer.querySelector('.js-visual-token'); + expect(token.classList.contains('search-token-milestone')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.querySelector('.name').innerText).toEqual('milestone'); expect(token.querySelector('.value')).toEqual(null); @@ -307,6 +303,7 @@ describe('Filtered Search Visual Tokens', () => { subject.addVisualTokenElement('label', 'Frontend'); const token = tokensContainer.querySelector('.js-visual-token'); + expect(token.classList.contains('search-token-label')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.querySelector('.name').innerText).toEqual('label'); expect(token.querySelector('.value').innerText).toEqual('Frontend'); @@ -322,10 +319,12 @@ describe('Filtered Search Visual Tokens', () => { const labelToken = tokens[0]; const assigneeToken = tokens[1]; + expect(labelToken.classList.contains('search-token-label')).toEqual(true); expect(labelToken.classList.contains('filtered-search-token')).toEqual(true); expect(labelToken.querySelector('.name').innerText).toEqual('label'); expect(labelToken.querySelector('.value').innerText).toEqual('Frontend'); + expect(assigneeToken.classList.contains('search-token-assignee')).toEqual(true); expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true); expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee'); expect(assigneeToken.querySelector('.value').innerText).toEqual('@root'); @@ -685,349 +684,21 @@ describe('Filtered Search Visual Tokens', () => { }); describe('renderVisualTokenValue', () => { - const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search'); - const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken( - 'milestone', - 'upcoming', - ); - - let updateLabelTokenColorSpy; - let updateUserTokenAppearanceSpy; - beforeEach(() => { tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` ${authorToken.outerHTML} ${bugLabelToken.outerHTML} - ${keywordToken.outerHTML} - ${milestoneToken.outerHTML} `); - - spyOn(subject, 'updateLabelTokenColor'); - updateLabelTokenColorSpy = subject.updateLabelTokenColor; - - spyOn(subject, 'updateUserTokenAppearance'); - updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance; }); it('renders a author token value element', () => { - const { tokenNameElement, tokenValueContainer, tokenValueElement } = findElements( - authorToken, - ); + const { tokenNameElement, tokenValueElement } = findElements(authorToken); const tokenName = tokenNameElement.innerText; const tokenValue = 'new value'; subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); expect(tokenValueElement.innerText).toBe(tokenValue); - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1); - const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue]; - - expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs); - expect(updateLabelTokenColorSpy.calls.count()).toBe(0); - }); - - it('renders a label token value element', () => { - const { tokenNameElement, tokenValueContainer, tokenValueElement } = findElements( - bugLabelToken, - ); - const tokenName = tokenNameElement.innerText; - const tokenValue = 'new value'; - - subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue); - - expect(tokenValueElement.innerText).toBe(tokenValue); - expect(updateLabelTokenColorSpy.calls.count()).toBe(1); - const expectedArgs = [tokenValueContainer, tokenValue]; - - expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); - }); - - it('renders a milestone token value element', () => { - const { tokenNameElement, tokenValueElement } = findElements(milestoneToken); - const tokenName = tokenNameElement.innerText; - const tokenValue = 'new value'; - - subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue); - - expect(tokenValueElement.innerText).toBe(tokenValue); - expect(updateLabelTokenColorSpy.calls.count()).toBe(0); - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); - }); - - it('does not update user token appearance for `None` filter', () => { - const { tokenNameElement } = findElements(authorToken); - - const tokenName = tokenNameElement.innerText; - const tokenValue = 'None'; - - subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); - - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); - }); - - it('does not update user token appearance for `none` filter', () => { - const { tokenNameElement } = findElements(authorToken); - - const tokenName = tokenNameElement.innerText; - const tokenValue = 'none'; - - subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); - - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); - }); - - it('does not update user token appearance for `any` filter', () => { - const { tokenNameElement } = findElements(authorToken); - - const tokenName = tokenNameElement.innerText; - const tokenValue = 'any'; - - subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); - - expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); - }); - - it('does not update label token color for `none` filter', () => { - const { tokenNameElement } = findElements(bugLabelToken); - - const tokenName = tokenNameElement.innerText; - const tokenValue = 'none'; - - subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue); - - expect(updateLabelTokenColorSpy.calls.count()).toBe(0); - }); - - it('does not update label token color for `any` filter', () => { - const { tokenNameElement } = findElements(bugLabelToken); - - const tokenName = tokenNameElement.innerText; - const tokenValue = 'any'; - - subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue); - - expect(updateLabelTokenColorSpy.calls.count()).toBe(0); - }); - }); - - describe('updateUserTokenAppearance', () => { - let usersCacheSpy; - - beforeEach(() => { - spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username)); - }); - - it('ignores error if UsersCache throws', done => { - spyOn(window, 'Flash'); - const dummyError = new Error('Earth rotated backwards'); - const { tokenValueContainer, tokenValueElement } = findElements(authorToken); - const tokenValue = tokenValueElement.innerText; - usersCacheSpy = username => { - expect(`@${username}`).toBe(tokenValue); - return Promise.reject(dummyError); - }; - - subject - .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) - .then(() => { - expect(window.Flash.calls.count()).toBe(0); - }) - .then(done) - .catch(done.fail); - }); - - it('does nothing if user cannot be found', done => { - const { tokenValueContainer, tokenValueElement } = findElements(authorToken); - const tokenValue = tokenValueElement.innerText; - usersCacheSpy = username => { - expect(`@${username}`).toBe(tokenValue); - return Promise.resolve(undefined); - }; - - subject - .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) - .then(() => { - expect(tokenValueElement.innerText).toBe(tokenValue); - }) - .then(done) - .catch(done.fail); - }); - - it('replaces author token with avatar and display name', done => { - const dummyUser = { - name: 'Important Person', - avatar_url: 'https://host.invalid/mypics/avatar.png', - }; - const { tokenValueContainer, tokenValueElement } = findElements(authorToken); - const tokenValue = tokenValueElement.innerText; - usersCacheSpy = username => { - expect(`@${username}`).toBe(tokenValue); - return Promise.resolve(dummyUser); - }; - - subject - .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) - .then(() => { - expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue); - expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); - const avatar = tokenValueElement.querySelector('img.avatar'); - - expect(avatar.src).toBe(dummyUser.avatar_url); - expect(avatar.alt).toBe(''); - }) - .then(done) - .catch(done.fail); - }); - - it('escapes user name when creating token', done => { - const dummyUser = { - name: '<script>', - avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`, - }; - const { tokenValueContainer, tokenValueElement } = findElements(authorToken); - const tokenValue = tokenValueElement.innerText; - usersCacheSpy = username => { - expect(`@${username}`).toBe(tokenValue); - return Promise.resolve(dummyUser); - }; - - subject - .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) - .then(() => { - expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); - tokenValueElement.querySelector('.avatar').remove(); - - expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name)); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('setTokenStyle', () => { - let originalTextColor; - - beforeEach(() => { - originalTextColor = bugLabelToken.style.color; - }); - - it('should set backgroundColor', () => { - const originalBackgroundColor = bugLabelToken.style.backgroundColor; - const token = subject.setTokenStyle(bugLabelToken, 'blue', 'white'); - - expect(token.style.backgroundColor).toEqual('blue'); - expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor); - }); - - it('should set textColor', () => { - const token = subject.setTokenStyle(bugLabelToken, 'white', 'black'); - - expect(token.style.color).toEqual('black'); - expect(token.style.color).not.toEqual(originalTextColor); - }); - - it('should add inverted class when textColor is #FFFFFF', () => { - const token = subject.setTokenStyle(bugLabelToken, 'black', '#FFFFFF'); - - expect(token.style.color).toEqual('rgb(255, 255, 255)'); - expect(token.style.color).not.toEqual(originalTextColor); - expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true); - }); - }); - - describe('updateLabelTokenColor', () => { - const jsonFixtureName = 'labels/project_labels.json'; - const dummyEndpoint = '/dummy/endpoint'; - - preloadFixtures(jsonFixtureName); - - let labelData; - - beforeAll(() => { - labelData = getJSONFixture(jsonFixtureName); - }); - - const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( - 'label', - '~doesnotexist', - ); - const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( - 'label', - '~"some space"', - ); - - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${bugLabelToken.outerHTML} - ${missingLabelToken.outerHTML} - ${spaceLabelToken.outerHTML} - `); - - const filteredSearchInput = document.querySelector('.filtered-search'); - filteredSearchInput.dataset.baseEndpoint = dummyEndpoint; - - AjaxCache.internalStorage = {}; - AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData; - }); - - const parseColor = color => { - const dummyElement = document.createElement('div'); - dummyElement.style.color = color; - return dummyElement.style.color; - }; - - const expectValueContainerStyle = (tokenValueContainer, label) => { - expect(tokenValueContainer.getAttribute('style')).not.toBe(null); - expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); - expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); - }; - - const findLabel = tokenValue => - labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`); - - it('updates the color of a label token', done => { - const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); - const tokenValue = tokenValueElement.innerText; - const matchingLabel = findLabel(tokenValue); - - subject - .updateLabelTokenColor(tokenValueContainer, tokenValue) - .then(() => { - expectValueContainerStyle(tokenValueContainer, matchingLabel); - }) - .then(done) - .catch(done.fail); - }); - - it('updates the color of a label token with spaces', done => { - const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken); - const tokenValue = tokenValueElement.innerText; - const matchingLabel = findLabel(tokenValue); - - subject - .updateLabelTokenColor(tokenValueContainer, tokenValue) - .then(() => { - expectValueContainerStyle(tokenValueContainer, matchingLabel); - }) - .then(done) - .catch(done.fail); - }); - - it('does not change color of a missing label', done => { - const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken); - const tokenValue = tokenValueElement.innerText; - const matchingLabel = findLabel(tokenValue); - - expect(matchingLabel).toBe(undefined); - - subject - .updateLabelTokenColor(tokenValueContainer, tokenValue) - .then(() => { - expect(tokenValueContainer.getAttribute('style')).toBe(null); - }) - .then(done) - .catch(done.fail); }); }); }); diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js new file mode 100644 index 00000000000..f52dc26a7bb --- /dev/null +++ b/spec/javascripts/filtered_search/visual_token_value_spec.js @@ -0,0 +1,361 @@ +import VisualTokenValue from '~/filtered_search/visual_token_value'; +import _ from 'underscore'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import UsersCache from '~/lib/utils/users_cache'; +import DropdownUtils from '~/filtered_search//dropdown_utils'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; + +describe('Filtered Search Visual Tokens', () => { + const findElements = tokenElement => { + const tokenNameElement = tokenElement.querySelector('.name'); + const tokenValueContainer = tokenElement.querySelector('.value-container'); + const tokenValueElement = tokenValueContainer.querySelector('.value'); + const tokenType = tokenNameElement.innerText.toLowerCase(); + const tokenValue = tokenValueElement.innerText; + const subject = new VisualTokenValue(tokenValue, tokenType); + return { subject, tokenValueContainer, tokenValueElement }; + }; + + let tokensContainer; + let authorToken; + let bugLabelToken; + + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + ${FilteredSearchSpecHelper.createInputHTML()} + </ul> + `); + tokensContainer = document.querySelector('.tokens-container'); + + authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user'); + bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); + }); + + describe('updateUserTokenAppearance', () => { + let usersCacheSpy; + + beforeEach(() => { + spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username)); + }); + + it('ignores error if UsersCache throws', done => { + spyOn(window, 'Flash'); + const dummyError = new Error('Earth rotated backwards'); + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = username => { + expect(`@${username}`).toBe(tokenValue); + return Promise.reject(dummyError); + }; + + subject + .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(window.Flash.calls.count()).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + + it('does nothing if user cannot be found', done => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = username => { + expect(`@${username}`).toBe(tokenValue); + return Promise.resolve(undefined); + }; + + subject + .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(tokenValueElement.innerText).toBe(tokenValue); + }) + .then(done) + .catch(done.fail); + }); + + it('replaces author token with avatar and display name', done => { + const dummyUser = { + name: 'Important Person', + avatar_url: 'https://host.invalid/mypics/avatar.png', + }; + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = username => { + expect(`@${username}`).toBe(tokenValue); + return Promise.resolve(dummyUser); + }; + + subject + .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue); + expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); + const avatar = tokenValueElement.querySelector('img.avatar'); + + expect(avatar.src).toBe(dummyUser.avatar_url); + expect(avatar.alt).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + + it('escapes user name when creating token', done => { + const dummyUser = { + name: '<script>', + avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`, + }; + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + const tokenValue = tokenValueElement.innerText; + usersCacheSpy = username => { + expect(`@${username}`).toBe(tokenValue); + return Promise.resolve(dummyUser); + }; + + subject + .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) + .then(() => { + expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); + tokenValueElement.querySelector('.avatar').remove(); + + expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name)); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateLabelTokenColor', () => { + const jsonFixtureName = 'labels/project_labels.json'; + const dummyEndpoint = '/dummy/endpoint'; + + preloadFixtures(jsonFixtureName); + + let labelData; + + beforeAll(() => { + labelData = getJSONFixture(jsonFixtureName); + }); + + const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( + 'label', + '~doesnotexist', + ); + const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( + 'label', + '~"some space"', + ); + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${bugLabelToken.outerHTML} + ${missingLabelToken.outerHTML} + ${spaceLabelToken.outerHTML} + `); + + const filteredSearchInput = document.querySelector('.filtered-search'); + filteredSearchInput.dataset.baseEndpoint = dummyEndpoint; + + AjaxCache.internalStorage = {}; + AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData; + }); + + const parseColor = color => { + const dummyElement = document.createElement('div'); + dummyElement.style.color = color; + return dummyElement.style.color; + }; + + const expectValueContainerStyle = (tokenValueContainer, label) => { + expect(tokenValueContainer.getAttribute('style')).not.toBe(null); + expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color)); + expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color)); + }; + + const findLabel = tokenValue => + labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`); + + it('updates the color of a label token', done => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + + subject + .updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expectValueContainerStyle(tokenValueContainer, matchingLabel); + }) + .then(done) + .catch(done.fail); + }); + + it('updates the color of a label token with spaces', done => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + + subject + .updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expectValueContainerStyle(tokenValueContainer, matchingLabel); + }) + .then(done) + .catch(done.fail); + }); + + it('does not change color of a missing label', done => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(missingLabelToken); + const tokenValue = tokenValueElement.innerText; + const matchingLabel = findLabel(tokenValue); + + expect(matchingLabel).toBe(undefined); + + subject + .updateLabelTokenColor(tokenValueContainer, tokenValue) + .then(() => { + expect(tokenValueContainer.getAttribute('style')).toBe(null); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('setTokenStyle', () => { + let originalTextColor; + + beforeEach(() => { + originalTextColor = bugLabelToken.style.color; + }); + + it('should set backgroundColor', () => { + const originalBackgroundColor = bugLabelToken.style.backgroundColor; + const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'blue', 'white'); + + expect(token.style.backgroundColor).toEqual('blue'); + expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor); + }); + + it('should set textColor', () => { + const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'white', 'black'); + + expect(token.style.color).toEqual('black'); + expect(token.style.color).not.toEqual(originalTextColor); + }); + + it('should add inverted class when textColor is #FFFFFF', () => { + const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'black', '#FFFFFF'); + + expect(token.style.color).toEqual('rgb(255, 255, 255)'); + expect(token.style.color).not.toEqual(originalTextColor); + expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true); + }); + }); + + describe('render', () => { + const setupSpies = subject => { + spyOn(subject, 'updateLabelTokenColor'); // eslint-disable-line jasmine/no-unsafe-spy + const updateLabelTokenColorSpy = subject.updateLabelTokenColor; + + spyOn(subject, 'updateUserTokenAppearance'); // eslint-disable-line jasmine/no-unsafe-spy + const updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance; + + return { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy }; + }; + + const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search'); + const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken( + 'milestone', + 'upcoming', + ); + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${authorToken.outerHTML} + ${bugLabelToken.outerHTML} + ${keywordToken.outerHTML} + ${milestoneToken.outerHTML} + `); + }); + + it('renders a author token value element', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + + const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1); + const expectedArgs = [tokenValueContainer, tokenValueElement]; + + expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs); + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); + + it('renders a label token value element', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + + const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(1); + const expectedArgs = [tokenValueContainer]; + + expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs); + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('renders a milestone token value element', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(milestoneToken); + + const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('does not update user token appearance for `none` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + + subject.tokenType = 'none'; + + const { updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('does not update user token appearance for `any` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + + subject.tokenType = 'any'; + + const { updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('does not update label token color for `none` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + + subject.tokenType = 'none'; + + const { updateLabelTokenColorSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); + + it('does not update label token color for `any` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + + subject.tokenType = 'any'; + + const { updateLabelTokenColorSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js index 8933dd5def4..fd06bb1f324 100644 --- a/spec/javascripts/helpers/filtered_search_spec_helper.js +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -5,7 +5,7 @@ export default class FilteredSearchSpecHelper { static createFilterVisualToken(name, value, isSelected = false) { const li = document.createElement('li'); - li.classList.add('js-visual-token', 'filtered-search-token'); + li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`); li.innerHTML = ` <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb index 41e6fb47b11..9945d598a14 100644 --- a/spec/lib/api/helpers/custom_validators_spec.rb +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -50,6 +50,29 @@ describe API::Helpers::CustomValidators do end end + describe API::Helpers::CustomValidators::ArrayNoneAny do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => [] }) + expect_no_validation_error({ 'test' => [1, 2, 3] }) + expect_no_validation_error({ 'test' => 'None' }) + expect_no_validation_error({ 'test' => 'Any' }) + expect_no_validation_error({ 'test' => 'none' }) + expect_no_validation_error({ 'test' => 'any' }) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_other_string' }) + end + end + end + def expect_no_validation_error(params) expect { validate_test_param!(params) }.not_to raise_error end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index c9b93a84aef..8c2fc048a54 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -9,6 +9,7 @@ describe Gitlab::ProjectTemplate do described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'), described_class.new('iosswift', 'iOS (Swift)', 'A ready-to-go template for use with iOS Swift apps.', 'https://gitlab.com/gitlab-org/project-templates/iosswift'), described_class.new('dotnetcore', '.NET Core', 'A .NET Core console application template, customizable for any .NET Core project', 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'), + described_class.new('android', 'Android', 'A ready-to-go template for use with Android apps.', 'https://gitlab.com/gitlab-org/project-templates/android'), described_class.new('gomicro', 'Go Micro', 'Go Micro is a framework for micro service development.', 'https://gitlab.com/gitlab-org/project-templates/go-micro'), described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'), described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'), diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index d5fd42509a3..a40fa988287 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -56,6 +56,14 @@ describe Clusters::Applications::Ingress do expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) end end + + context 'when there is already an external_hostname' do + let(:application) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) + end + end end describe '#install_command' do diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 6e58f3ad699..2967c4076c6 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -26,6 +26,13 @@ describe Clusters::Applications::Jupyter do it { expect(jupyter).to be_installable } end + + context 'when ingress is installed and external_hostname is assigned' do + let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') } + let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) } + + it { expect(jupyter).to be_installable } + end end describe '#install_command' do diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 4884a5927fb..bf425a2617c 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -64,6 +64,14 @@ describe Clusters::Applications::Knative do expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) end end + + context 'when there is already an external_hostname' do + let(:application) { create(:clusters_applications_knative, :installed, external_hostname: 'localhost.localdomain') } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) + end + end end shared_examples 'a command' do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 3feed4e9718..acbcdc7d170 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -31,6 +31,7 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix } it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix } + it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix } it { is_expected.to respond_to :project } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 07cb4c9c1e3..a35d3f14df8 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -270,6 +270,25 @@ describe MergeRequest do end end + describe '.recent_target_branches' do + let(:project) { create(:project) } + let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') } + let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') } + let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') } + let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') } + + before do + merge_request1.update_columns(updated_at: 1.day.since) + merge_request2.update_columns(updated_at: 2.days.since) + merge_request3.update_columns(updated_at: 3.days.since) + merge_request4.update_columns(updated_at: 4.days.since) + end + + it 'returns target branches sort by updated at desc' do + expect(described_class.recent_target_branches).to match_array(['feature', 'merge-test', 'fix']) + end + end + describe '#target_branch_sha' do let(:project) { create(:project, :repository) } diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb index db3df760472..6d34b0a8b4b 100644 --- a/spec/policies/issuable_policy_spec.rb +++ b/spec/policies/issuable_policy_spec.rb @@ -13,7 +13,7 @@ describe IssuablePolicy, models: true do context 'when user is able to read project' do it 'enables user to read and update issuables' do - expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request) + expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request) end end @@ -24,12 +24,12 @@ describe IssuablePolicy, models: true do it 'enables user to read and update issuables' do project.add_maintainer(user) - expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request) + expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request) end end it 'disallows user from reading and updating issuables from that project' do - expect(policies).to be_disallowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request) + expect(policies).to be_disallowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request) end end end diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb new file mode 100644 index 00000000000..1efa70addc2 --- /dev/null +++ b/spec/policies/merge_request_policy_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe MergeRequestPolicy do + let(:guest) { create(:user) } + let(:author) { create(:user) } + let(:developer) { create(:user) } + let(:project) { create(:project, :public) } + + def permissions(user, merge_request) + described_class.new(user, merge_request) + end + + before do + project.add_guest(guest) + project.add_guest(author) + project.add_developer(developer) + end + + context 'when merge request is unlocked' do + let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) } + + it 'allows author to reopen merge request' do + expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request) + end + + it 'allows developer to reopen merge request' do + expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request) + end + + it 'prevents guest from reopening merge request' do + expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request) + end + end + + context 'when merge request is locked' do + let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) } + + it 'prevents author from reopening merge request' do + expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + + it 'prevents developer from reopening merge request' do + expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + + it 'prevents guests from reopening merge request' do + expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) + end + end +end diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb index f3036fbcb0e..80fc48d1b07 100644 --- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -6,9 +6,17 @@ describe Clusters::Applications::CheckIngressIpAddressService do let(:application) { create(:clusters_applications_ingress, :installed) } let(:service) { described_class.new(application) } let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) } - let(:ingress) { [{ ip: '111.222.111.222' }] } let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" } + let(:ingress) do + [ + { + ip: '111.222.111.222', + hostname: 'localhost.localdomain' + } + ] + end + let(:kube_service) do ::Kubeclient::Resource.new( { diff --git a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb index 14638a574a5..02de47a96dd 100644 --- a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb +++ b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb @@ -12,6 +12,14 @@ shared_examples 'check ingress ip executions' do |app_name| end end + context 'when the ingress external hostname is available' do + it 'updates the external_hostname for the app' do + subject + + expect(application.external_hostname).to eq('localhost.localdomain') + end + end + context 'when the ingress ip address is not available' do let(:ingress) { nil } diff --git a/spec/validators/devise_email_validator_spec.rb b/spec/validators/devise_email_validator_spec.rb new file mode 100644 index 00000000000..7860b659bd3 --- /dev/null +++ b/spec/validators/devise_email_validator_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeviseEmailValidator do + let!(:user) { build(:user, public_email: 'test@example.com') } + subject { validator.validate(user) } + + describe 'validations' do + context 'by default' do + let(:validator) { described_class.new(attributes: [:public_email]) } + + it 'allows when email is valid' do + subject + + expect(user.errors).to be_empty + end + + it 'returns error when email is invalid' do + user.public_email = 'invalid' + + subject + + expect(user.errors).to be_present + expect(user.errors.first[1]).to eq 'is invalid' + end + + it 'returns error when email is nil' do + user.public_email = nil + + subject + + expect(user.errors).to be_present + end + + it 'returns error when email is blank' do + user.public_email = '' + + subject + + expect(user.errors).to be_present + expect(user.errors.first[1]).to eq 'is invalid' + end + end + end + + context 'when regexp is set as Regexp' do + let(:validator) { described_class.new(attributes: [:public_email], regexp: /[0-9]/) } + + it 'allows when value match' do + user.public_email = '1' + + subject + + expect(user.errors).to be_empty + end + + it 'returns error when value does not match' do + subject + + expect(user.errors).to be_present + end + end + + context 'when regexp is set as String' do + it 'raise argument error' do + expect { described_class.new( { regexp: 'something' } ) }.to raise_error ArgumentError + end + end + + context 'when allow_nil is set to true' do + let(:validator) { described_class.new(attributes: [:public_email], allow_nil: true) } + + it 'allows when email is nil' do + user.public_email = nil + + subject + + expect(user.errors).to be_empty + end + end + + context 'when allow_blank is set to true' do + let(:validator) { described_class.new(attributes: [:public_email], allow_blank: true) } + + it 'allows when email is blank' do + user.public_email = '' + + subject + + expect(user.errors).to be_empty + end + end +end diff --git a/vendor/project_templates/android.tar.gz b/vendor/project_templates/android.tar.gz Binary files differnew file mode 100644 index 00000000000..3df17a0d9a6 --- /dev/null +++ b/vendor/project_templates/android.tar.gz |