diff options
-rw-r--r-- | .gitlab/ci/notifications.gitlab-ci.yml | 4 | ||||
-rw-r--r-- | app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue | 14 | ||||
-rw-r--r-- | doc/api/access_requests.md | 48 | ||||
-rw-r--r-- | doc/development/README.md | 1 | ||||
-rw-r--r-- | doc/development/auto_devops.md | 42 | ||||
-rw-r--r-- | doc/topics/autodevops/index.md | 18 | ||||
-rw-r--r-- | qa/qa/page/merge_request/show.rb | 24 | ||||
-rw-r--r-- | spec/frontend/commit/__snapshots__/commit_pipeline_status_component_spec.js.snap | 39 | ||||
-rw-r--r-- | spec/frontend/commit/commit_pipeline_status_component_spec.js | 152 | ||||
-rw-r--r-- | spec/javascripts/commit/commit_pipeline_status_component_spec.js | 106 |
10 files changed, 281 insertions, 167 deletions
diff --git a/.gitlab/ci/notifications.gitlab-ci.yml b/.gitlab/ci/notifications.gitlab-ci.yml index e4ffceb2dc0..5d7dd752fd5 100644 --- a/.gitlab/ci/notifications.gitlab-ci.yml +++ b/.gitlab/ci/notifications.gitlab-ci.yml @@ -11,7 +11,7 @@ schedule:package-and-qa:notify-success: - .only-canonical-schedules - .notify script: - - 'scripts/notify-slack qa-master ":tada: Scheduled QA against `master` passed! :tada: See $CI_PIPELINE_URL." ci_passing' + - 'scripts/notify-slack qa-master ":tada: Scheduled QA against master passed! :tada: See $CI_PIPELINE_URL." ci_passing' needs: ["schedule:package-and-qa"] when: on_success @@ -20,6 +20,6 @@ schedule:package-and-qa:notify-failure: - .only-canonical-schedules - .notify script: - - 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against `master` failed! :skull_and_crossbones: See $CI_PIPELINE_URL." ci_failing' + - 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL." ci_failing' needs: ["schedule:package-and-qa"] when: on_failure diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 12ee1ce2f0c..60fd3ed5ea7 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -21,14 +21,6 @@ export default { type: String, required: true, }, - /* This prop can be used to replace some of the `render_commit_status` - used across GitLab, this way we could use this vue component and add a - realtime status where it makes sense - realtime: { - type: Boolean, - required: false, - default: true, - }, */ }, data() { return { @@ -47,6 +39,9 @@ export default { this.service = new CommitPipelineService(this.endpoint); this.initPolling(); }, + beforeDestroy() { + this.poll.stop(); + }, methods: { successCallback(res) { const { pipelines } = res.data; @@ -95,9 +90,6 @@ export default { .catch(this.errorCallback); }, }, - destroy() { - this.poll.stop(); - }, }; </script> <template> diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md index 973c3968d90..584a4ecb89c 100644 --- a/doc/api/access_requests.md +++ b/doc/api/access_requests.md @@ -6,7 +6,7 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized: -``` +```plaintext 10 => Guest access 20 => Reporter access 30 => Developer access @@ -18,14 +18,16 @@ Gets a list of access requests viewable by the authenticated user. -``` +```plaintext GET /groups/:id/access_requests GET /projects/:id/access_requests ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | + +Example request: ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/access_requests @@ -59,14 +61,16 @@ Example response: Requests access for the authenticated user to a group or project. -``` +```plaintext POST /groups/:id/access_requests POST /projects/:id/access_requests ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | + +Example request: ```bash curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/access_requests @@ -90,16 +94,18 @@ Example response: Approves an access request for the given user. -``` +```plaintext PUT /groups/:id/access_requests/:user_id/approve PUT /projects/:id/access_requests/:user_id/approve ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `user_id` | integer | yes | The user ID of the access requester | -| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) | +| Attribute | Type | Required | Description | +| -------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `user_id` | integer | yes | The user ID of the access requester | +| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) | + +Example request: ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id/approve?access_level=20 @@ -123,15 +129,17 @@ Example response: Denies an access request for the given user. -``` +```plaintext DELETE /groups/:id/access_requests/:user_id DELETE /projects/:id/access_requests/:user_id ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `user_id` | integer | yes | The user ID of the access requester | +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `user_id` | integer | yes | The user ID of the access requester | + +Example request: ```bash curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/access_requests/:user_id diff --git a/doc/development/README.md b/doc/development/README.md index bdc92236716..6480d40303b 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -68,6 +68,7 @@ description: 'Learn how to contribute to GitLab.' - [Git LFS](lfs.md) - [Developing against interacting components or features](interacting_components.md) - [File uploads](uploads.md) +- [Auto DevOps development guide](auto_devops.md) ## Performance guides diff --git a/doc/development/auto_devops.md b/doc/development/auto_devops.md new file mode 100644 index 00000000000..f88bcc9cfb8 --- /dev/null +++ b/doc/development/auto_devops.md @@ -0,0 +1,42 @@ +# Auto DevOps development guide + +This document provides a development guide for contributors to +[Auto DevOps](../topics/autodevops/index.md) + +## Development + +Auto DevOps builds on top of GitLab CI to create an automatic pipeline +based on your project contents. When Auto DevOps is enabled for a +project, the user does not need to explicitly include any pipeline configuration +through a [`.gitlab-ci.yml` file](../ci/yaml/README.md). + +In the absence of a `.gitlab-ci.yml` file, the [Auto DevOps CI +template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) +is used implicitly to configure the pipeline for the project. This +template is a top-level template that includes other sub-templates, +which then defines jobs. + +Some jobs use images that are built from external projects: + +- [Auto Build](../topics/autodevops/index.md#auto-build) uses + [configuration](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml) + in which the `build` job uses an image that is built using the + [`auto-build-image`](https://gitlab.com/gitlab-org/cluster-integration/auto-build-image) + project. +- [Auto Deploy](../topics/autodevops/index.md#auto-deploy) uses + [configuration](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml) + in which the jobs defined in this template use an image that is built using the + [`auto-deploy-image`](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image) + project. By default, the Helm chart defined in + [`auto-deploy-app`](https://gitlab.com/gitlab-org/charts/auto-deploy-app) + is used to deploy. + +There are extra variables that get passed to the CI jobs when Auto +DevOps is enabled that are not present in a normal CI job. These can be +found in +[`ProjectAutoDevops`](https://gitlab.com/gitlab-org/gitlab/blob/bf69484afa94e091c3e1383945f60dbe4e8681af/app/models/project_auto_devops.rb). + +## Development environment + +Configuring [GDK for Auto +DevOps](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md). diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 662cc838bdb..802effcb7a2 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -1183,22 +1183,6 @@ As of GitLab 10.0, the supported buildpacks are: The following restrictions apply. -### Private project support - -CAUTION: **Caution:** Private project support in Auto DevOps is experimental. - -When a project has been marked as private, GitLab's [Container -Registry][container-registry] requires authentication when downloading -containers. Auto DevOps will automatically provide the required authentication -information to Kubernetes, allowing temporary access to the registry. -Authentication credentials will be valid while the pipeline is running, allowing -for a successful initial deployment. - -After the pipeline completes, Kubernetes will no longer be able to access the -Container Registry. **Restarting a pod, scaling a service, or other actions which -require on-going access to the registry may fail**. On-going secure access is -planned for a subsequent release. - ### Private registry support There is no documented way of using private container registry with Auto DevOps. @@ -1274,4 +1258,4 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ ## Development guides -Configuring [GDK for Auto DevOps](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md). +[Development guide for Auto DevOps](../../development/auto_devops.md) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index ea0cbfe2ab0..6b4a8bacf24 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -129,17 +129,7 @@ module QA end def try_to_merge! - # The merge button is disabled on load - wait do - has_element?(:merge_button) - end - - # The merge button is enabled via JS - wait(reload: false) do - !find_element(:merge_button).disabled? - end - - merge_immediately + merge_immediately if ready_to_merge? end def merge! @@ -187,6 +177,18 @@ module QA click_element :edit_button end + def ready_to_merge? + # The merge button is disabled on load + wait do + has_element?(:merge_button) + end + + # The merge button is enabled via JS + wait(reload: false) do + !find_element(:merge_button).disabled? + end + end + def view_email_patches click_element :dropdown_toggle visit_link_in_element(:download_email_patches) diff --git a/spec/frontend/commit/__snapshots__/commit_pipeline_status_component_spec.js.snap b/spec/frontend/commit/__snapshots__/commit_pipeline_status_component_spec.js.snap new file mode 100644 index 00000000000..9199db69fed --- /dev/null +++ b/spec/frontend/commit/__snapshots__/commit_pipeline_status_component_spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Commit pipeline status component when polling is not successful renders not found CI icon without loader 1`] = ` +<div + class="ci-status-link" +> + <a> + <ciicon-stub + aria-label="Pipeline: not found" + cssclasses="" + data-container="body" + data-original-title="Pipeline: not found" + size="24" + status="[object Object]" + title="" + /> + </a> +</div> +`; + +exports[`Commit pipeline status component when polling is successful renders CI icon without loader 1`] = ` +<div + class="ci-status-link" +> + <a + href="/frontend-fixtures/pipelines-project/pipelines/47" + > + <ciicon-stub + aria-label="Pipeline: pending" + cssclasses="" + data-container="body" + data-original-title="Pipeline: pending" + size="24" + status="[object Object]" + title="" + /> + </a> +</div> +`; diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js new file mode 100644 index 00000000000..1768fd745c9 --- /dev/null +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -0,0 +1,152 @@ +import Visibility from 'visibilityjs'; +import { GlLoadingIcon } from '@gitlab/ui'; +import Poll from '~/lib/utils/poll'; +import flash from '~/flash'; +import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; +import { shallowMount } from '@vue/test-utils'; +import { getJSONFixture } from '../helpers/fixtures'; + +jest.mock('~/lib/utils/poll'); +jest.mock('visibilityjs'); +jest.mock('~/flash'); + +const mockFetchData = jest.fn(); +jest.mock('~/projects/tree/services/commit_pipeline_service', () => + jest.fn().mockImplementation(() => ({ + fetchData: mockFetchData.mockReturnValue(Promise.resolve()), + })), +); + +describe('Commit pipeline status component', () => { + let wrapper; + const { pipelines } = getJSONFixture('pipelines/pipelines.json'); + const { status: mockCiStatus } = pipelines[0].details; + + const defaultProps = { + endpoint: 'endpoint', + }; + + const createComponent = (props = {}) => { + wrapper = shallowMount(CommitPipelineStatus, { + propsData: { + ...defaultProps, + ...props, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + jest.clearAllMocks(); + }); + + describe('Visibility management', () => { + describe('when component is hidden', () => { + beforeEach(() => { + Visibility.hidden.mockReturnValue(true); + createComponent(); + }); + + it('does not start polling', () => { + const [pollInstance] = Poll.mock.instances; + expect(pollInstance.makeRequest).not.toHaveBeenCalled(); + }); + + it('requests pipeline data', () => { + expect(mockFetchData).toHaveBeenCalled(); + }); + }); + + describe('when component is visible', () => { + beforeEach(() => { + Visibility.hidden.mockReturnValue(false); + createComponent(); + }); + + it('starts polling', () => { + const [pollInstance] = [...Poll.mock.instances].reverse(); + expect(pollInstance.makeRequest).toHaveBeenCalled(); + }); + }); + + describe('when component changes its visibility', () => { + it.each` + visibility | action + ${false} | ${'restart'} + ${true} | ${'stop'} + `( + '$action polling when component visibility becomes $visibility', + ({ visibility, action }) => { + Visibility.hidden.mockReturnValue(!visibility); + createComponent(); + const [pollInstance] = Poll.mock.instances; + expect(pollInstance[action]).not.toHaveBeenCalled(); + Visibility.hidden.mockReturnValue(visibility); + const [visibilityHandler] = Visibility.change.mock.calls[0]; + visibilityHandler(); + expect(pollInstance[action]).toHaveBeenCalled(); + }, + ); + }); + }); + + it('stops polling when component is destroyed', () => { + createComponent(); + wrapper.destroy(); + const [pollInstance] = Poll.mock.instances; + expect(pollInstance.stop).toHaveBeenCalled(); + }); + + describe('when polling', () => { + let pollConfig; + beforeEach(() => { + Poll.mockImplementation(config => { + pollConfig = config; + return { makeRequest: jest.fn(), restart: jest.fn(), stop: jest.fn() }; + }); + createComponent(); + }); + + it('shows the loading icon at start', () => { + createComponent(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + + pollConfig.successCallback({ + data: { pipelines: [] }, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + }); + }); + + describe('is successful', () => { + beforeEach(() => { + pollConfig.successCallback({ + data: { pipelines: [{ details: { status: mockCiStatus } }] }, + }); + return wrapper.vm.$nextTick(); + }); + + it('renders CI icon without loader', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('is not successful', () => { + beforeEach(() => { + pollConfig.errorCallback(); + }); + + it('renders not found CI icon without loader', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('displays flash error message', () => { + expect(flash).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js deleted file mode 100644 index f6b36e88a5f..00000000000 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('Commit pipeline status component', () => { - let vm; - let Component; - let mock; - const mockCiStatus = { - details_path: '/root/hello-world/pipelines/1', - favicon: 'canceled.ico', - group: 'canceled', - has_details: true, - icon: 'status_canceled', - label: 'canceled', - text: 'canceled', - }; - - beforeEach(() => { - Component = Vue.extend(commitPipelineStatus); - }); - - describe('While polling pipeline data successfully', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet('/dummy/endpoint').reply(() => { - const res = Promise.resolve([ - 200, - { - pipelines: [ - { - details: { - status: mockCiStatus, - }, - }, - ], - }, - ]); - return res; - }); - vm = mountComponent(Component, { - endpoint: '/dummy/endpoint', - }); - }); - - afterEach(() => { - vm.poll.stop(); - vm.$destroy(); - mock.restore(); - }); - - it('shows the loading icon when polling is starting', done => { - expect(vm.$el.querySelector('.loading-container')).not.toBe(null); - setTimeout(() => { - expect(vm.$el.querySelector('.loading-container')).toBe(null); - done(); - }); - }); - - it('contains a ciStatus when the polling is successful ', done => { - setTimeout(() => { - expect(vm.ciStatus).toEqual(mockCiStatus); - done(); - }); - }); - - it('contains a ci-status icon when polling is successful', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null); - expect(vm.$el.querySelector('.ci-status-icon').classList).toContain( - `ci-status-icon-${mockCiStatus.group}`, - ); - done(); - }); - }); - }); - - describe('When polling data was not successful', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet('/dummy/endpoint').reply(502, {}); - vm = new Component({ - props: { - endpoint: '/dummy/endpoint', - }, - }); - }); - - afterEach(() => { - vm.poll.stop(); - vm.$destroy(); - mock.restore(); - }); - - it('calls an errorCallback', done => { - spyOn(vm, 'errorCallback').and.callThrough(); - vm.$mount(); - setTimeout(() => { - expect(vm.errorCallback.calls.count()).toEqual(1); - done(); - }); - }); - }); -}); |