diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-14 09:07:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-14 09:07:54 +0000 |
commit | 5ff1b520badaa2da217416964709f49f3ede350a (patch) | |
tree | 821ddbc4062a56fd2a7f26a0776457da3e074859 | |
parent | eccfaf7c242ab8afec22cdaf68865763e780fdeb (diff) | |
download | gitlab-ce-5ff1b520badaa2da217416964709f49f3ede350a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
33 files changed, 949 insertions, 36 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fdd4c2a64f6..ba840d1ced0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33" +image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33" stages: - prepare diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 87023da3c24..008151f889f 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -14,10 +14,10 @@ variables: GIT_STRATEGY: none environment: - name: review-docs/$CI_COMMIT_REF_SLUG + name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables # Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693 - url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX + url: http://docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup before_script: # We don't clone the repo by using GIT_STRATEGY: none and only download the @@ -39,7 +39,7 @@ review-docs-deploy: review-docs-cleanup: extends: .review-docs environment: - name: review-docs/$CI_COMMIT_REF_SLUG + name: review-docs/$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID action: stop script: - ./trigger-build-docs cleanup diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index adbfe2b4675..a3a2ab0691f 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -13,7 +13,7 @@ - .default-before_script - .assets-compile-cache - .only-code-qa-changes - image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1 + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1 stage: test dependencies: ["setup-test-env"] needs: ["setup-test-env"] diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 0c0591d3fdc..af7c7a0d152 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -123,7 +123,7 @@ - name: redis:alpine .use-pg10: - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -137,7 +137,7 @@ - name: docker.elastic.co/elasticsearch/elasticsearch:5.6.12 .use-pg10-ee: - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -465,7 +465,7 @@ gem 'lograge', '~> 0.5' gem 'grape_logging', '~> 1.7' # DNS Lookup -gem 'net-dns', '~> 0.9.0' +gem 'gitlab-net-dns', '~> 0.9.1' # Countries list gem 'countries', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index c6828ecb2cd..4285cfac45e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -370,6 +370,7 @@ GEM redis (> 3.0.0, < 5.0.0) gitlab-license (1.0.0) gitlab-markup (1.7.0) + gitlab-net-dns (0.9.1) gitlab-peek (0.0.1) railties (>= 4.0.0) gitlab-sidekiq-fetcher (0.5.2) @@ -596,7 +597,6 @@ GEM mustermann (~> 1.0.0) nakayoshi_fork (0.0.4) nap (1.1.0) - net-dns (0.9.0) net-ldap (0.16.0) net-ntp (2.1.3) net-ssh (5.2.0) @@ -1173,6 +1173,7 @@ DEPENDENCIES gitlab-labkit (~> 0.5) gitlab-license (~> 1.0) gitlab-markup (~> 1.7.0) + gitlab-net-dns (~> 0.9.1) gitlab-peek (~> 0.0.1) gitlab-sidekiq-fetcher (= 0.5.2) gitlab-styles (~> 2.7) @@ -1222,7 +1223,6 @@ DEPENDENCIES mini_magick minitest (~> 5.11.0) nakayoshi_fork (~> 0.0.4) - net-dns (~> 0.9.0) net-ldap net-ntp net-ssh (~> 5.2) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue new file mode 100644 index 00000000000..dc766176617 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue @@ -0,0 +1,40 @@ +<script> +import { GlLink } from '@gitlab/ui'; + +export default { + components: { + GlLink, + }, + props: { + artifacts: { + type: Array, + required: true, + }, + }, +}; +</script> +<template> + <table class="table m-0"> + <thead class="thead-white text-nowrap"> + <tr class="d-none d-sm-table-row"> + <th class="w-0"></th> + <th>{{ __('Artifact') }}</th> + <th class="w-50"></th> + <th>{{ __('Job') }}</th> + </tr> + </thead> + + <tbody> + <tr v-for="item in artifacts" :key="item.text"> + <td class="w-0"></td> + <td> + <gl-link :href="item.url" target="_blank">{{ item.text }}</gl-link> + </td> + <td class="w-0"></td> + <td> + <gl-link :href="item.job_path">{{ item.job_name }}</gl-link> + </td> + </tr> + </tbody> + </table> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue new file mode 100644 index 00000000000..730e67761be --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue @@ -0,0 +1,36 @@ +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import ArtifactsList from './artifacts_list.vue'; +import MrCollapsibleExtension from './mr_collapsible_extension.vue'; +import createStore from '../stores/artifacts_list'; + +export default { + store: createStore(), + components: { + ArtifactsList, + MrCollapsibleExtension, + }, + props: { + endpoint: { + type: String, + required: true, + }, + }, + computed: { + ...mapState(['artifacts', 'isLoading', 'hasError']), + ...mapGetters(['title']), + }, + created() { + this.setEndpoint(this.endpoint); + this.fetchArtifacts(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchArtifacts']), + }, +}; +</script> +<template> + <mr-collapsible-extension :title="title" :is-loading="isLoading" :has-error="hasError"> + <artifacts-list :artifacts="artifacts" /> + </mr-collapsible-extension> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue new file mode 100644 index 00000000000..ae1d9368008 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue @@ -0,0 +1,81 @@ +<script> +import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlButton, + GlLink, + GlLoadingIcon, + Icon, + }, + props: { + title: { + type: String, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + hasError: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isCollapsed: true, + }; + }, + + computed: { + arrowIconName() { + return this.isCollapsed ? 'angle-right' : 'angle-down'; + }, + ariaLabel() { + return this.isCollapsed ? __('Expand') : __('Collapse'); + }, + isButtonDisabled() { + return this.isLoading || this.hasError; + }, + }, + methods: { + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + }, +}; +</script> +<template> + <div> + <div class="mr-widget-extension d-flex align-items-center pl-3"> + <gl-button + class="btn-blank btn s32 square append-right-default" + :aria-label="ariaLabel" + :disabled="isButtonDisabled" + @click="toggleCollapsed" + > + <gl-loading-icon v-if="isLoading" /> + <icon v-else :name="arrowIconName" class="js-icon" /> + </gl-button> + <gl-button + variant="link" + class="js-title" + :disabled="isButtonDisabled" + :class="{ 'border-0': isButtonDisabled }" + @click="toggleCollapsed" + > + <template v-if="isCollapsed">{{ title }}</template> + <template v-else>{{ __('Collapse') }}</template> + </gl-button> + </div> + + <div v-if="!isCollapsed" class="border-top js-slot-container"> + <slot></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 8fdf61a6b8d..ffc3e0967d4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,5 +1,6 @@ <script> import _ from 'underscore'; +import ArtifactsApp from './artifacts_list_app.vue'; import Deployment from './deployment.vue'; import MrWidgetContainer from './mr_widget_container.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue'; @@ -15,6 +16,7 @@ import MrWidgetPipeline from './mr_widget_pipeline.vue'; export default { name: 'MrWidgetPipelineContainer', components: { + ArtifactsApp, Deployment, MrWidgetContainer, MrWidgetPipeline, @@ -79,6 +81,9 @@ export default { :troubleshooting-docs-path="mr.troubleshootingDocsPath" /> <template v-slot:footer> + <div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts"> + <artifacts-app :endpoint="mr.exposedArtifactsPath" /> + </div> <div v-if="deployments.length" class="mr-widget-extension"> <deployment v-for="deployment in deployments" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js new file mode 100644 index 00000000000..3648db795f5 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js @@ -0,0 +1,74 @@ +import Visibility from 'visibilityjs'; +import axios from '~/lib/utils/axios_utils'; +import Poll from '~/lib/utils/poll'; +import httpStatusCodes from '~/lib/utils/http_status'; + +import * as types from './mutation_types'; + +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +export const requestArtifacts = ({ commit }) => commit(types.REQUEST_ARTIFACTS); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const fetchArtifacts = ({ state, dispatch }) => { + dispatch('requestArtifacts'); + + eTagPoll = new Poll({ + resource: { + getArtifacts(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.endpoint, + method: 'getArtifacts', + successCallback: ({ data, status }) => { + dispatch('receiveArtifactsSuccess', { + data, + status, + }); + }, + errorCallback: () => dispatch('receiveArtifactsError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.endpoint) + .then(({ data, status }) => dispatch('receiveArtifactsSuccess', { data, status })) + .catch(() => dispatch('receiveArtifactsError')); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveArtifactsSuccess = ({ commit }, response) => { + // With 204 we keep polling and don't update the state + if (response.status === httpStatusCodes.OK) { + commit(types.RECEIVE_ARTIFACTS_SUCCESS, response.data); + } +}; + +export const receiveArtifactsError = ({ commit }) => commit(types.RECEIVE_ARTIFACTS_ERROR); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js new file mode 100644 index 00000000000..8921637b93b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js @@ -0,0 +1,16 @@ +import { s__, n__ } from '~/locale'; + +export const title = state => { + if (state.isLoading) { + return s__('BuildArtifacts|Loading artifacts'); + } + + if (state.hasError) { + return s__('BuildArtifacts|An error occurred while fetching the artifacts'); + } + + return n__('View exposed artifact', 'View %d exposed artifacts', state.artifacts.length); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js new file mode 100644 index 00000000000..f8abbc99f0f --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import * as getters from './getters'; +import state from './state'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js new file mode 100644 index 00000000000..282faf6f8a4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; + +export const REQUEST_ARTIFACTS = 'REQUEST_ARTIFACTS'; +export const RECEIVE_ARTIFACTS_SUCCESS = 'RECEIVE_ARTIFACTS_SUCCESS'; +export const RECEIVE_ARTIFACTS_ERROR = 'RECEIVE_ARTIFACTS_ERROR'; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js new file mode 100644 index 00000000000..95a091f1bd6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/mutations.js @@ -0,0 +1,22 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.REQUEST_ARTIFACTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_ARTIFACTS_SUCCESS](state, response) { + state.hasError = false; + state.isLoading = false; + + state.artifacts = response; + }, + [types.RECEIVE_ARTIFACTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + + state.artifacts = []; + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js new file mode 100644 index 00000000000..92dad171b1b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/state.js @@ -0,0 +1,8 @@ +export default () => ({ + endpoint: null, + + isLoading: false, + hasError: false, + + artifacts: [], +}); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 699d41494bf..99d32ffe424 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -100,6 +100,7 @@ export default class MergeRequestStore { this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; this.testResultsPath = data.test_reports_path; + this.exposedArtifactsPath = data.exposed_artifacts_path; this.cancelAutoMergePath = data.cancel_auto_merge_path; this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index cdc894ee5a0..b8a421ac9d3 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -508,8 +508,7 @@ .btn-group %a.btn Edit %a.btn.btn-danger Remove - .file-contenta.code - = render 'shared/file_highlight', blob: blob + = render 'shared/file_highlight', blob: blob %h2#markdown Markdown %h4 diff --git a/changelogs/unreleased/15018-build-results-fe.yml b/changelogs/unreleased/15018-build-results-fe.yml new file mode 100644 index 00000000000..6d619910937 --- /dev/null +++ b/changelogs/unreleased/15018-build-results-fe.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vue and Vuex app to render exposed artifacts +merge_request: 17934 +author: +type: added diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index b534e84191a..5dcdf0e85e9 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -377,6 +377,14 @@ The certificate to be used needs to be installed on all Gitaly nodes and on all client nodes that communicate with it following the procedure described in [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). +NOTE: **Note** +The self-signed certificate must specify the address you use to access the +Gitaly server. If you are addressing the Gitaly server by a hostname, you can +either use the Common Name field for this, or add it as a Subject Alternative +Name. If you are addressing the Gitaly server by its IP address, you must add it +as a Subject Alternative Name to the certificate. +[gRPC does not support using an IP address as Common Name in a certificate](https://github.com/grpc/grpc/issues/2691). + NOTE: **Note:** It is possible to configure Gitaly servers with both an unencrypted listening address `listen_addr` and an encrypted listening diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 9dead5cc0e2..fb0aa5130f8 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -300,17 +300,17 @@ You will need to push a branch to those repositories, it doesn't work for forks. The `review-docs-deploy*` job will: 1. Create a new branch in the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) - project named after the scheme: `$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG`, - where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ce` for - CE, etc. -1. Trigger a cross project pipeline and build the docs site with your changes - -After a few minutes, the Review App will be deployed and you will be able to -preview the changes. The docs URL can be found in two places: - -- In the merge request widget -- In the output of the `review-docs-deploy*` job, which also includes the - triggered pipeline so that you can investigate whether something went wrong + project named after the scheme: `docs-preview-$DOCS_GITLAB_REPO_SUFFIX-$CI_MERGE_REQUEST_IID`, + where `DOCS_GITLAB_REPO_SUFFIX` is the suffix for each product, e.g, `ee` for + EE, `omnibus` for Omnibus GitLab, etc, and `CI_MERGE_REQUEST_IID` is the ID + of the respective merge request. +1. Trigger a cross project pipeline and build the docs site with your changes. + +In case the review app URL returns 404, this means that either the site is not +yet deployed, or something went wrong with the remote pipeline. Give it a few +minutes and it should appear online, otherwise you can check the status of the +remote pipeline from the link in the merge request's job output. +If the pipeline failed or got stuck, drop a line in the `#docs` chat channel. TIP: **Tip:** Someone with no merge rights to the GitLab projects (think of forks from @@ -343,12 +343,11 @@ If you want to know the in-depth details, here's what's really happening: 1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/trigger-build-docs) script with the `deploy` flag, which in turn: 1. Takes your branch name and applies the following: - - The slug of the branch name is used to avoid special characters since - ultimately this will be used by NGINX. - - The `preview-` prefix is added to avoid conflicts if there's a remote branch - with the same name that you created in the merge request. - - The final branch name is truncated to 42 characters to avoid filesystem - limitations with long branch names (> 63 chars). + - The `docs-preview-` prefix is added. + - The product slug is used to know the project the review app originated + from. + - The number of the merge request is added so that you can know by the + `gitlab-docs` branch name the merge request it originated from. 1. The remote branch is then created if it doesn't exist (meaning you can re-run the manual job as many times as you want and this step will be skipped). 1. A new cross-project pipeline is triggered in the docs project. @@ -369,6 +368,7 @@ The following GitLab features are used among others: - [Review Apps](../../ci/review_apps/index.md) - [Artifacts](../../ci/yaml/README.md#artifacts) - [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects) +- [Pipelines for merge requests](../../ci/merge_request_pipelines/index.md) ## Testing diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 5ec68ef3251..f2df6209492 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -38,7 +38,7 @@ The current stages are: ## Default image The default image is currently -`dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`. +`gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`. It includes Ruby 2.6.3, Go 1.11, Git 2.22, Chrome 73, Node 12, Yarn 1.16, PostgreSQL 9.6, and Graphics Magick 1.3.33. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cd9208a434a..ae3c240419b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1964,6 +1964,9 @@ msgstr "" msgid "Arrange charts" msgstr "" +msgid "Artifact" +msgstr "" + msgid "Artifact ID" msgstr "" @@ -2661,6 +2664,12 @@ msgstr "" msgid "Browse files" msgstr "" +msgid "BuildArtifacts|An error occurred while fetching the artifacts" +msgstr "" + +msgid "BuildArtifacts|Loading artifacts" +msgstr "" + msgid "Built-in" msgstr "" @@ -18067,6 +18076,11 @@ msgstr "" msgid "View epics list" msgstr "" +msgid "View exposed artifact" +msgid_plural "View %d exposed artifacts" +msgstr[0] "" +msgstr[1] "" + msgid "View file @ " msgstr "" diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs index 046ee5bceb8..ea717d9ccbf 100755 --- a/scripts/trigger-build-docs +++ b/scripts/trigger-build-docs @@ -16,14 +16,12 @@ end GITLAB_DOCS_REPO = 'gitlab-org/gitlab-docs'.freeze # -# Truncate the remote docs branch name otherwise we hit the filesystem -# limit and the directory name where NGINX serves the site won't match -# the branch name. +# This is the branch that will be created in the gitlab-docs project. +# Name it after the product we're previewing and the ID of the MR that +# kicked the review app. # def docs_branch - # The maximum string length a file can have on a filesystem (ext4) - # is 63 characters. CI_ENVIRONMENT_SLUG is limited to 24 characters. - ENV["CI_ENVIRONMENT_SLUG"] + "docs-preview-#{slug}-#{ENV["CI_MERGE_REQUEST_IID"]}" end # diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js new file mode 100644 index 00000000000..49ed796d9a8 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js @@ -0,0 +1,121 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue'; +import createStore from '~/vue_merge_request_widget/stores/artifacts_list'; +import { artifactsList } from './mock_data'; + +describe('Merge Requests Artifacts list app', () => { + let wrapper; + let mock; + const store = createStore(); + const localVue = createLocalVue(); + localVue.use(Vuex); + + const actionSpies = { + fetchArtifacts: jest.fn(), + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + const createComponent = () => { + wrapper = mount(localVue.extend(ArtifactsListApp), { + propsData: { + endpoint: TEST_HOST, + }, + store, + methods: { + ...actionSpies, + }, + localVue, + sync: false, + }); + }; + + const findButtons = () => wrapper.findAll('button'); + const findTitle = () => wrapper.find('.js-title'); + const findTableRows = () => wrapper.findAll('tbody tr'); + + describe('while loading', () => { + beforeEach(() => { + createComponent(); + store.dispatch('requestArtifacts'); + return wrapper.vm.$nextTick(); + }); + + it('renders a loading icon', () => { + const loadingIcon = wrapper.find(GlLoadingIcon); + expect(loadingIcon.exists()).toBe(true); + }); + + it('renders loading text', () => { + expect(findTitle().text()).toBe('Loading artifacts'); + }); + + it('renders disabled buttons', () => { + const buttons = findButtons(); + expect(buttons.at(0).attributes('disabled')).toBe('disabled'); + expect(buttons.at(1).attributes('disabled')).toBe('disabled'); + }); + }); + + describe('with results', () => { + beforeEach(() => { + createComponent(); + mock.onGet(wrapper.vm.$store.state.endpoint).reply(200, artifactsList, {}); + store.dispatch('receiveArtifactsSuccess', { + data: artifactsList, + status: 200, + }); + return wrapper.vm.$nextTick(); + }); + + it('renders a title with the number of artifacts', () => { + expect(findTitle().text()).toBe('View 2 exposed artifacts'); + }); + + it('renders both buttons enabled', () => { + const buttons = findButtons(); + expect(buttons.at(0).attributes('disabled')).toBe(undefined); + expect(buttons.at(1).attributes('disabled')).toBe(undefined); + }); + + describe('on click', () => { + it('renders the list of artifacts', () => { + findTitle().trigger('click'); + wrapper.vm.$nextTick(() => { + expect(findTableRows().length).toEqual(2); + }); + }); + }); + }); + + describe('with error', () => { + beforeEach(() => { + createComponent(); + mock.onGet(wrapper.vm.$store.state.endpoint).reply(500, {}, {}); + store.dispatch('receiveArtifactsError'); + return wrapper.vm.$nextTick(); + }); + + it('renders the error state', () => { + expect(findTitle().text()).toBe('An error occurred while fetching the artifacts'); + }); + + it('renders disabled buttons', () => { + const buttons = findButtons(); + expect(buttons.at(0).attributes('disabled')).toBe('disabled'); + expect(buttons.at(1).attributes('disabled')).toBe('disabled'); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js new file mode 100644 index 00000000000..8c805faf574 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js @@ -0,0 +1,61 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import ArtifactsList from '~/vue_merge_request_widget/components/artifacts_list.vue'; +import { artifactsList } from './mock_data'; + +describe('Artifacts List', () => { + let wrapper; + const localVue = createLocalVue(); + + const data = { + artifacts: artifactsList, + }; + + const mountComponent = props => { + wrapper = shallowMount(localVue.extend(ArtifactsList), { + propsData: { + ...props, + }, + sync: false, + localVue, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + mountComponent(data); + }); + + it('renders list of artifacts', () => { + expect(wrapper.findAll('tbody tr').length).toEqual(data.artifacts.length); + }); + + it('renders link for the artifact', () => { + expect(wrapper.find(GlLink).attributes('href')).toEqual(data.artifacts[0].url); + }); + + it('renders artifact name', () => { + expect(wrapper.find(GlLink).text()).toEqual(data.artifacts[0].text); + }); + + it('renders job url', () => { + expect( + wrapper + .findAll(GlLink) + .at(1) + .attributes('href'), + ).toEqual(data.artifacts[0].job_path); + }); + + it('renders job name', () => { + expect( + wrapper + .findAll(GlLink) + .at(1) + .text(), + ).toEqual(data.artifacts[0].job_name); + }); +}); diff --git a/spec/frontend/vue_mr_widget/components/mock_data.js b/spec/frontend/vue_mr_widget/components/mock_data.js new file mode 100644 index 00000000000..39c7d75cda5 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/mock_data.js @@ -0,0 +1,15 @@ +// eslint-disable-next-line import/prefer-default-export +export const artifactsList = [ + { + text: 'result.txt', + url: 'bar', + job_name: 'generate-artifact', + job_path: 'bar', + }, + { + text: 'foo.txt', + url: 'foo', + job_name: 'foo-artifact', + job_path: 'foo', + }, +]; diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js new file mode 100644 index 00000000000..4c9507223a1 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js @@ -0,0 +1,105 @@ +import { mount } from '@vue/test-utils'; +import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; + +describe('Merge Request Collapsible Extension', () => { + let wrapper; + const data = { + title: 'View artifacts', + }; + + const mountComponent = props => { + wrapper = mount(MrCollapsibleSection, { + propsData: { + ...props, + }, + slots: { + default: '<div class="js-slot">Foo</div>', + }, + }); + }; + + const findTitle = () => wrapper.find('.js-title'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('while collapsed', () => { + beforeEach(() => { + mountComponent(data); + }); + + it('renders provided title', () => { + expect(findTitle().text()).toBe(data.title); + }); + + it('renders angle-right icon', () => { + expect(wrapper.vm.arrowIconName).toBe('angle-right'); + }); + + describe('onClick', () => { + beforeEach(() => { + wrapper.find('button').trigger('click'); + }); + + it('rendes the provided slot', () => { + expect(wrapper.find('.js-slot').isVisible()).toBe(true); + }); + + it('renders `Collapse` as the title', () => { + expect(findTitle().text()).toBe('Collapse'); + }); + + it('renders angle-down icon', () => { + expect(wrapper.vm.arrowIconName).toBe('angle-down'); + }); + }); + }); + + describe('while loading', () => { + beforeEach(() => { + mountComponent(Object.assign({}, data, { isLoading: true })); + }); + + it('renders the buttons disabled', () => { + expect( + wrapper + .findAll('button') + .at(0) + .attributes('disabled'), + ).toEqual('disabled'); + expect( + wrapper + .findAll('button') + .at(1) + .attributes('disabled'), + ).toEqual('disabled'); + }); + + it('renders loading spinner', () => { + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + }); + }); + + describe('with error', () => { + beforeEach(() => { + mountComponent(Object.assign({}, data, { hasError: true })); + }); + + it('renders the buttons disabled', () => { + expect( + wrapper + .findAll('button') + .at(0) + .attributes('disabled'), + ).toEqual('disabled'); + expect( + wrapper + .findAll('button') + .at(1) + .attributes('disabled'), + ).toEqual('disabled'); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js new file mode 100644 index 00000000000..62ee6f5f189 --- /dev/null +++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js @@ -0,0 +1,32 @@ +import { title } from '~/vue_merge_request_widget/stores/artifacts_list/getters'; +import state from '~/vue_merge_request_widget/stores/artifacts_list/state'; +import { artifactsList } from '../../components/mock_data'; + +describe('Artifacts Store Getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('title', () => { + describe('when is loading', () => { + it('returns loading message', () => { + localState.isLoading = true; + expect(title(localState)).toBe('Loading artifacts'); + }); + }); + describe('when has error', () => { + it('returns error message', () => { + localState.hasError = true; + expect(title(localState)).toBe('An error occurred while fetching the artifacts'); + }); + }); + describe('when it has artifacts', () => { + it('returns artifacts message', () => { + localState.artifacts = artifactsList; + expect(title(localState)).toBe('View 2 exposed artifacts'); + }); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js new file mode 100644 index 00000000000..ea89fdb72e9 --- /dev/null +++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js @@ -0,0 +1,78 @@ +import state from '~/vue_merge_request_widget/stores/artifacts_list/state'; +import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations'; +import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types'; + +describe('Artifacts Store Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json'); + + expect(stateCopy.endpoint).toEqual('endpoint.json'); + }); + }); + + describe('REQUEST_ARTIFACTS', () => { + it('should set isLoading to true', () => { + mutations[types.REQUEST_ARTIFACTS](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('REECEIVE_ARTIFACTS_SUCCESS', () => { + const artifacts = [ + { + text: 'result.txt', + url: 'asda', + job_name: 'generate-artifact', + job_path: 'asda', + }, + { + text: 'file.txt', + url: 'asda', + job_name: 'generate-artifact', + job_path: 'asda', + }, + ]; + + beforeEach(() => { + mutations[types.RECEIVE_ARTIFACTS_SUCCESS](stateCopy, artifacts); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to false', () => { + expect(stateCopy.hasError).toEqual(false); + }); + + it('should set list of artifacts', () => { + expect(stateCopy.artifacts).toEqual(artifacts); + }); + }); + + describe('RECEIVE_ARTIFACTS_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_ARTIFACTS_ERROR](stateCopy); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.hasError).toEqual(true); + }); + + it('should set list of artifacts as empty array', () => { + expect(stateCopy.artifacts).toEqual([]); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js index dfbc68c48b9..6cdf60f3535 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js @@ -1,6 +1,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue'; import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue'; import { mockStore } from '../mock_data'; describe('MrWidgetPipelineContainer', () => { @@ -87,4 +88,10 @@ describe('MrWidgetPipelineContainer', () => { expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps); }); }); + + describe('with artifacts path', () => { + it('renders the artifacts app', () => { + expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 2f79806652b..089ec08fbf9 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -289,4 +289,5 @@ export const mockStore = { troubleshootingDocsPath: 'troubleshooting-docs-path', ciStatus: 'ci-status', hasCI: true, + exposedArtifactsPath: 'exposed_artifacts.json', }; diff --git a/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js new file mode 100644 index 00000000000..4c4bebcb4cd --- /dev/null +++ b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js @@ -0,0 +1,165 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { + setEndpoint, + requestArtifacts, + clearEtagPoll, + stopPolling, + fetchArtifacts, + receiveArtifactsSuccess, + receiveArtifactsError, +} from '~/vue_merge_request_widget/stores/artifacts_list/actions'; +import state from '~/vue_merge_request_widget/stores/artifacts_list/state'; +import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('Artifacts App Store Actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setEndpoint', () => { + it('should commit SET_ENDPOINT mutation', done => { + testAction( + setEndpoint, + 'endpoint.json', + mockedState, + [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }], + [], + done, + ); + }); + }); + + describe('requestArtifacts', () => { + it('should commit REQUEST_ARTIFACTS mutation', done => { + testAction( + requestArtifacts, + null, + mockedState, + [{ type: types.REQUEST_ARTIFACTS }], + [], + done, + ); + }); + }); + + describe('fetchArtifacts', () => { + let mock; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [ + { + text: 'result.txt', + url: 'asda', + job_name: 'generate-artifact', + job_path: 'asda', + }, + ]); + + testAction( + fetchArtifacts, + null, + mockedState, + [], + [ + { + type: 'requestArtifacts', + }, + { + payload: { + data: [ + { + text: 'result.txt', + url: 'asda', + job_name: 'generate-artifact', + job_path: 'asda', + }, + ], + status: 200, + }, + type: 'receiveArtifactsSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches requestArtifacts and receiveArtifactsError ', done => { + testAction( + fetchArtifacts, + null, + mockedState, + [], + [ + { + type: 'requestArtifacts', + }, + { + type: 'receiveArtifactsError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveArtifactsSuccess', () => { + it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => { + testAction( + receiveArtifactsSuccess, + { data: { summary: {} }, status: 200 }, + mockedState, + [{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }], + [], + done, + ); + }); + + it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => { + testAction( + receiveArtifactsSuccess, + { data: { summary: {} }, status: 204 }, + mockedState, + [], + [], + done, + ); + }); + }); + + describe('receiveArtifactsError', () => { + it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => { + testAction( + receiveArtifactsError, + null, + mockedState, + [{ type: types.RECEIVE_ARTIFACTS_ERROR }], + [], + done, + ); + }); + }); +}); |