Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1f60908cc0
commit
22622fab4a
10 changed files with 615 additions and 143 deletions
|
@ -19,14 +19,14 @@ the noise (due to constantly failing tests, flaky tests, and so on) so that new
|
||||||
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
|
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
|
||||||
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
||||||
- [ ] Quarantine test check-list
|
- [ ] Quarantine test check-list
|
||||||
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#quarantining-tests).
|
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantining-tests).
|
||||||
- [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#quarantined-test-types).
|
- [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantined-test-types).
|
||||||
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
|
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
|
||||||
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
|
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
|
||||||
- [ ] Dequarantine test check-list
|
- [ ] Dequarantine test check-list
|
||||||
- [ ] Follow the [Dequarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#dequarantining-tests).
|
- [ ] Follow the [Dequarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#dequarantining-tests).
|
||||||
- [ ] Confirm the test consistently passes on the target GitLab environment(s).
|
- [ ] Confirm the test consistently passes on the target GitLab environment(s).
|
||||||
- [ ] (Optionally) [Trigger a manual GitLab-QA pipeline](https://about.gitlab.com/handbook/engineering/quality/guidelines/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release) against a specific GitLab environment using the `RELEASE` variable from the `package-and-qa` job of the current merge request.
|
- [ ] (Optionally) [Trigger a manual GitLab-QA pipeline](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release) against a specific GitLab environment using the `RELEASE` variable from the `package-and-qa` job of the current merge request.
|
||||||
- [ ] To ensure a faster turnaround, ask in the `#quality` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
|
- [ ] To ensure a faster turnaround, ask in the `#quality` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
|
||||||
|
|
||||||
<!-- Base labels. -->
|
<!-- Base labels. -->
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTable, GlButton } from '@gitlab/ui';
|
import { GlTable, GlButton, GlPagination, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
|
||||||
|
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
|
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||||
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeployKeysTable',
|
name: 'DeployKeysTable',
|
||||||
i18n: {
|
i18n: {
|
||||||
pageTitle: __('Public deploy keys'),
|
pageTitle: __('Public deploy keys'),
|
||||||
newDeployKeyButtonText: __('New deploy key'),
|
newDeployKeyButtonText: __('New deploy key'),
|
||||||
|
emptyStateTitle: __('No public deploy keys'),
|
||||||
|
emptyStateDescription: __(
|
||||||
|
'Deploy keys grant read/write access to all repositories in your instance',
|
||||||
|
),
|
||||||
|
remove: __('Remove deploy key'),
|
||||||
|
edit: __('Edit deploy key'),
|
||||||
|
pagination: {
|
||||||
|
next: __('Next'),
|
||||||
|
prev: __('Prev'),
|
||||||
|
},
|
||||||
|
apiErrorMessage: __('An error occurred fetching the public deploy keys. Please try again.'),
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
@ -29,13 +44,83 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
label: __('Actions'),
|
label: __('Actions'),
|
||||||
|
tdClass: 'gl-lg-w-1px gl-white-space-nowrap',
|
||||||
|
thClass: 'gl-lg-w-1px gl-white-space-nowrap',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
DEFAULT_PER_PAGE,
|
||||||
components: {
|
components: {
|
||||||
GlTable,
|
GlTable,
|
||||||
GlButton,
|
GlButton,
|
||||||
|
GlPagination,
|
||||||
|
TimeAgoTooltip,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlEmptyState,
|
||||||
},
|
},
|
||||||
inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
|
inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
page: 1,
|
||||||
|
totalItems: 0,
|
||||||
|
loading: false,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
shouldShowTable() {
|
||||||
|
return this.totalItems !== 0 || this.loading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
page(newPage) {
|
||||||
|
this.fetchDeployKeys(newPage);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchDeployKeys();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editHref(id) {
|
||||||
|
return this.editPath.replace(':id', id);
|
||||||
|
},
|
||||||
|
projectHref(project) {
|
||||||
|
return `/${cleanLeadingSeparator(project.path_with_namespace)}`;
|
||||||
|
},
|
||||||
|
async fetchDeployKeys(page) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const { headers, data: items } = await Api.deployKeys({
|
||||||
|
page,
|
||||||
|
public: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.totalItems === 0) {
|
||||||
|
this.totalItems = parseInt(headers?.['x-total'], 10) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items = items.map(
|
||||||
|
({ id, title, fingerprint, projects_with_write_access, created_at }) => ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
fingerprint,
|
||||||
|
projects: projects_with_write_access,
|
||||||
|
created: created_at,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
createFlash({
|
||||||
|
message: this.$options.i18n.apiErrorMessage,
|
||||||
|
captureError: true,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.totalItems = 0;
|
||||||
|
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,10 +130,71 @@ export default {
|
||||||
<h4 class="gl-m-0">
|
<h4 class="gl-m-0">
|
||||||
{{ $options.i18n.pageTitle }}
|
{{ $options.i18n.pageTitle }}
|
||||||
</h4>
|
</h4>
|
||||||
<gl-button variant="confirm" :href="createPath">{{
|
<gl-button variant="confirm" :href="createPath" data-testid="new-deploy-key-button">{{
|
||||||
$options.i18n.newDeployKeyButtonText
|
$options.i18n.newDeployKeyButtonText
|
||||||
}}</gl-button>
|
}}</gl-button>
|
||||||
</div>
|
</div>
|
||||||
<gl-table :fields="$options.fields" data-testid="deploy-keys-list" />
|
<template v-if="shouldShowTable">
|
||||||
|
<gl-table
|
||||||
|
:busy="loading"
|
||||||
|
:items="items"
|
||||||
|
:fields="$options.fields"
|
||||||
|
stacked="lg"
|
||||||
|
data-testid="deploy-keys-list"
|
||||||
|
>
|
||||||
|
<template #table-busy>
|
||||||
|
<gl-loading-icon size="lg" class="gl-my-5" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(projects)="{ item: { projects } }">
|
||||||
|
<a
|
||||||
|
v-for="project in projects"
|
||||||
|
:key="project.id"
|
||||||
|
:href="projectHref(project)"
|
||||||
|
class="gl-display-block"
|
||||||
|
>{{ project.name_with_namespace }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(fingerprint)="{ item: { fingerprint } }">
|
||||||
|
<code>{{ fingerprint }}</code>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(created)="{ item: { created } }">
|
||||||
|
<time-ago-tooltip :time="created" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #head(actions)="{ label }">
|
||||||
|
<span class="gl-sr-only">{{ label }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(actions)="{ item: { id } }">
|
||||||
|
<gl-button
|
||||||
|
icon="pencil"
|
||||||
|
:aria-label="$options.i18n.edit"
|
||||||
|
:href="editHref(id)"
|
||||||
|
class="gl-mr-2"
|
||||||
|
/>
|
||||||
|
<gl-button variant="danger" icon="remove" :aria-label="$options.i18n.remove" />
|
||||||
|
</template>
|
||||||
|
</gl-table>
|
||||||
|
<gl-pagination
|
||||||
|
v-if="!loading"
|
||||||
|
v-model="page"
|
||||||
|
:per-page="$options.DEFAULT_PER_PAGE"
|
||||||
|
:total-items="totalItems"
|
||||||
|
:next-text="$options.i18n.pagination.next"
|
||||||
|
:prev-text="$options.i18n.pagination.prev"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<gl-empty-state
|
||||||
|
v-else
|
||||||
|
:svg-path="emptyStateSvgPath"
|
||||||
|
:title="$options.i18n.emptyStateTitle"
|
||||||
|
:description="$options.i18n.emptyStateDescription"
|
||||||
|
:primary-button-text="$options.i18n.newDeployKeyButtonText"
|
||||||
|
:primary-button-link="createPath"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -91,6 +91,7 @@ const Api = {
|
||||||
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
|
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
|
||||||
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
|
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
|
||||||
notificationSettingsPath: '/api/:version/notification_settings',
|
notificationSettingsPath: '/api/:version/notification_settings',
|
||||||
|
deployKeysPath: '/api/:version/deploy_keys',
|
||||||
|
|
||||||
group(groupId, callback = () => {}) {
|
group(groupId, callback = () => {}) {
|
||||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||||
|
@ -950,6 +951,12 @@ const Api = {
|
||||||
return axios.delete(url);
|
return axios.delete(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deployKeys(params = {}) {
|
||||||
|
const url = Api.buildUrl(this.deployKeysPath);
|
||||||
|
|
||||||
|
return axios.get(url, { params: { per_page: DEFAULT_PER_PAGE, ...params } });
|
||||||
|
},
|
||||||
|
|
||||||
async updateNotificationSettings(projectId, groupId, data = {}) {
|
async updateNotificationSettings(projectId, groupId, data = {}) {
|
||||||
let url = Api.buildUrl(this.notificationSettingsPath);
|
let url = Api.buildUrl(this.notificationSettingsPath);
|
||||||
|
|
||||||
|
|
|
@ -388,7 +388,7 @@ export default {
|
||||||
<div
|
<div
|
||||||
v-if="showMultiLineComment"
|
v-if="showMultiLineComment"
|
||||||
data-testid="multiline-comment"
|
data-testid="multiline-comment"
|
||||||
class="gl-mb-3 gl-text-gray-500 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
|
||||||
>
|
>
|
||||||
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
|
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
|
||||||
<template #startLine>
|
<template #startLine>
|
||||||
|
|
|
@ -50,6 +50,109 @@ the pipeline that finishes later creates the job artifact.
|
||||||
|
|
||||||
For more examples, view the [keyword reference for the `.gitlab-ci.yml` file](../yaml/index.md#artifacts).
|
For more examples, view the [keyword reference for the `.gitlab-ci.yml` file](../yaml/index.md#artifacts).
|
||||||
|
|
||||||
|
### Use CI/CD variables to define the artifacts name
|
||||||
|
|
||||||
|
You can use [CI/CD variables](../variables/index.md) to dynamically define the
|
||||||
|
artifacts file's name.
|
||||||
|
|
||||||
|
For example, to create an archive with a name of the current job:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "$CI_JOB_NAME"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
To create an archive with a name of the current branch or tag including only
|
||||||
|
the binaries directory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "$CI_COMMIT_REF_NAME"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
If your branch-name contains forward slashes
|
||||||
|
(for example `feature/my-feature`) it's advised to use `$CI_COMMIT_REF_SLUG`
|
||||||
|
instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
|
||||||
|
|
||||||
|
To create an archive with a name of the current job and the current branch or
|
||||||
|
tag including only the binaries directory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
To create an archive with a name of the current [stage](../yaml/index.md#stages) and branch name:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use **Windows Batch** to run your shell scripts you must replace
|
||||||
|
`$` with `%`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use **Windows PowerShell** to run your shell scripts you must replace
|
||||||
|
`$` with `$env:`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
job:
|
||||||
|
artifacts:
|
||||||
|
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exclude files from job artifacts
|
||||||
|
|
||||||
|
Use [`artifacts:exclude`](../yaml/index.md#artifactsexclude) to prevent files from
|
||||||
|
being added to an artifacts archive.
|
||||||
|
|
||||||
|
For example, to store all files in `binaries/`, but not `*.o` files located in
|
||||||
|
subdirectories of `binaries/`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
exclude:
|
||||||
|
- binaries/**/*.o
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike [`artifacts:paths`](../yaml/index.md#artifactspaths), `exclude` paths are not recursive.
|
||||||
|
To exclude all of the contents of a directory, match them explicitly rather
|
||||||
|
than matching the directory itself.
|
||||||
|
|
||||||
|
For example, to store all files in `binaries/` but nothing located in the `temp/` subdirectory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- binaries/
|
||||||
|
exclude:
|
||||||
|
- binaries/temp/**/*
|
||||||
|
```
|
||||||
|
|
||||||
## Download job artifacts
|
## Download job artifacts
|
||||||
|
|
||||||
You can download job artifacts or view the job archive:
|
You can download job artifacts or view the job archive:
|
||||||
|
@ -103,6 +206,35 @@ To delete a job:
|
||||||
1. On the top right of the job's log, select **Erase job log** (**{remove}**).
|
1. On the top right of the job's log, select **Erase job log** (**{remove}**).
|
||||||
1. On the confirmation dialog, select **OK**.
|
1. On the confirmation dialog, select **OK**.
|
||||||
|
|
||||||
|
## Expose job artifacts in the merge request UI
|
||||||
|
|
||||||
|
Use the [`artifacts:expose_as`](../yaml/index.md#artifactsexpose_as) keyword to expose
|
||||||
|
[job artifacts](../pipelines/job_artifacts.md) in the [merge request](../../user/project/merge_requests/index.md) UI.
|
||||||
|
|
||||||
|
For example, to match a single file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
test:
|
||||||
|
script: ["echo 'test' > file.txt"]
|
||||||
|
artifacts:
|
||||||
|
expose_as: 'artifact 1'
|
||||||
|
paths: ['file.txt']
|
||||||
|
```
|
||||||
|
|
||||||
|
With this configuration, GitLab adds a link **artifact 1** to the relevant merge request
|
||||||
|
that points to `file1.txt`. To access the link, select **View exposed artifact**
|
||||||
|
below the pipeline graph in the merge request overview.
|
||||||
|
|
||||||
|
An example that matches an entire directory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
test:
|
||||||
|
script: ["mkdir test && echo 'test' > test/file.txt"]
|
||||||
|
artifacts:
|
||||||
|
expose_as: 'artifact 1'
|
||||||
|
paths: ['test/']
|
||||||
|
```
|
||||||
|
|
||||||
## Retrieve job artifacts for other projects
|
## Retrieve job artifacts for other projects
|
||||||
|
|
||||||
To retrieve a job artifact from a different project, you might need to use a
|
To retrieve a job artifact from a different project, you might need to use a
|
||||||
|
|
|
@ -628,12 +628,13 @@ test_job_2:
|
||||||
|
|
||||||
### `artifacts`
|
### `artifacts`
|
||||||
|
|
||||||
Use `artifacts` to specify a list of files and directories that are
|
Use `artifacts` to specify which files to save as [job artifacts](../pipelines/job_artifacts.md).
|
||||||
|
Job artifacts are a list of files and directories that are
|
||||||
attached to the job when it [succeeds, fails, or always](#artifactswhen).
|
attached to the job when it [succeeds, fails, or always](#artifactswhen).
|
||||||
|
|
||||||
The artifacts are sent to GitLab after the job finishes. They are
|
The artifacts are sent to GitLab after the job finishes. They are
|
||||||
available for download in the GitLab UI if the size is not
|
available for download in the GitLab UI if the size is smaller than the
|
||||||
larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
|
the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
|
||||||
|
|
||||||
By default, jobs in later stages automatically download all the artifacts created
|
By default, jobs in later stages automatically download all the artifacts created
|
||||||
by jobs in earlier stages. You can control artifact download behavior in jobs with
|
by jobs in earlier stages. You can control artifact download behavior in jobs with
|
||||||
|
@ -652,16 +653,18 @@ artifacts are restored after [caches](#cache).
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15122) in GitLab 13.1
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15122) in GitLab 13.1
|
||||||
> - Requires GitLab Runner 13.1
|
> - Requires GitLab Runner 13.1
|
||||||
|
|
||||||
`exclude` makes it possible to prevent files from being added to an artifacts
|
Use `artifacts:exclude` to prevent files from being added to an artifacts archive.
|
||||||
archive.
|
|
||||||
|
|
||||||
Similar to [`artifacts:paths`](#artifactspaths), `exclude` paths are relative
|
**Keyword type**: Job keyword. You can use it only as part of a job or in the
|
||||||
to the project directory. You can use Wildcards that use
|
[`default:` section](#default).
|
||||||
[glob](https://en.wikipedia.org/wiki/Glob_(programming)) or
|
|
||||||
|
**Possible inputs**:
|
||||||
|
|
||||||
|
- An array of file paths, relative to the project directory.
|
||||||
|
- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) or
|
||||||
[`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#PathMatch) patterns.
|
[`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#PathMatch) patterns.
|
||||||
|
|
||||||
For example, to store all files in `binaries/`, but not `*.o` files located in
|
**Example of `artifacts:exclude`**:
|
||||||
subdirectories of `binaries/`:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@ -671,21 +674,19 @@ artifacts:
|
||||||
- binaries/**/*.o
|
- binaries/**/*.o
|
||||||
```
|
```
|
||||||
|
|
||||||
Unlike [`artifacts:paths`](#artifactspaths), `exclude` paths are not recursive. To exclude all of the contents of a directory, you can match them explicitly rather than matching the directory itself.
|
This example stores all files in `binaries/`, but not `*.o` files located in
|
||||||
|
subdirectories of `binaries/`.
|
||||||
|
|
||||||
For example, to store all files in `binaries/` but nothing located in the `temp/` subdirectory:
|
**Additional details**:
|
||||||
|
|
||||||
```yaml
|
- `artifacts:exclude` paths are not searched recursively.
|
||||||
artifacts:
|
- Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
exclude:
|
|
||||||
- binaries/temp/**/*
|
|
||||||
```
|
|
||||||
|
|
||||||
Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
|
|
||||||
`artifacts:exclude` too.
|
`artifacts:exclude` too.
|
||||||
|
|
||||||
|
**Related topics**:
|
||||||
|
|
||||||
|
- [Exclude files from job artifacts](../pipelines/job_artifacts.md#exclude-files-from-job-artifacts)
|
||||||
|
|
||||||
#### `artifacts:expire_in`
|
#### `artifacts:expire_in`
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) in GitLab 13.0 behind a disabled feature flag, the latest job artifacts are kept regardless of expiry time.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) in GitLab 13.0 behind a disabled feature flag, the latest job artifacts are kept regardless of expiry time.
|
||||||
|
@ -704,8 +705,14 @@ they expire and are deleted. The `expire_in` setting does not affect:
|
||||||
pipeline artifacts. See [When pipeline artifacts are deleted](../pipelines/pipeline_artifacts.md#when-pipeline-artifacts-are-deleted)
|
pipeline artifacts. See [When pipeline artifacts are deleted](../pipelines/pipeline_artifacts.md#when-pipeline-artifacts-are-deleted)
|
||||||
for more information.
|
for more information.
|
||||||
|
|
||||||
The value of `expire_in` is an elapsed time in seconds, unless a unit is provided. Valid values
|
After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
|
||||||
include:
|
accessible anymore.
|
||||||
|
|
||||||
|
**Keyword type**: Job keyword. You can use it only as part of a job or in the
|
||||||
|
[`default:` section](#default).
|
||||||
|
|
||||||
|
**Possible inputs**: The expiry time. If no unit is provided, the time is in seconds.
|
||||||
|
Valid values include:
|
||||||
|
|
||||||
- `'42'`
|
- `'42'`
|
||||||
- `42 seconds`
|
- `42 seconds`
|
||||||
|
@ -717,7 +724,7 @@ include:
|
||||||
- `3 weeks and 2 days`
|
- `3 weeks and 2 days`
|
||||||
- `never`
|
- `never`
|
||||||
|
|
||||||
To expire artifacts one week after being uploaded:
|
**Example of `artifacts:expire_in`**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
job:
|
job:
|
||||||
|
@ -725,28 +732,31 @@ job:
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
```
|
```
|
||||||
|
|
||||||
The expiration time period begins when the artifact is uploaded and stored on GitLab. If the expiry
|
**Additional details**:
|
||||||
time is not defined, it defaults to the
|
|
||||||
[instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration)
|
|
||||||
(30 days by default).
|
|
||||||
|
|
||||||
To override the expiration date and protect artifacts from being automatically deleted:
|
|
||||||
|
|
||||||
|
- The expiration time period begins when the artifact is uploaded and stored on GitLab.
|
||||||
|
If the expiry time is not defined, it defaults to the [instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration).
|
||||||
|
- To override the expiration date and protect artifacts from being automatically deleted:
|
||||||
- Select **Keep** on the job page.
|
- Select **Keep** on the job page.
|
||||||
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
|
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
|
||||||
`expire_in` to `never`.
|
`expire_in` to `never`.
|
||||||
|
|
||||||
After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
|
|
||||||
accessible anymore.
|
|
||||||
|
|
||||||
#### `artifacts:expose_as`
|
#### `artifacts:expose_as`
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15018) in GitLab 12.5.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15018) in GitLab 12.5.
|
||||||
|
|
||||||
Use the `expose_as` keyword to expose [job artifacts](../pipelines/job_artifacts.md)
|
Use the `artifacts:expose_as` keyword to
|
||||||
in the [merge request](../../user/project/merge_requests/index.md) UI.
|
[expose job artifacts in the merge request UI](../pipelines/job_artifacts.md#expose-job-artifacts-in-the-merge-request-ui).
|
||||||
|
|
||||||
For example, to match a single file:
|
**Keyword type**: Job keyword. You can use it only as part of a job or in the
|
||||||
|
[`default:` section](#default).
|
||||||
|
|
||||||
|
**Possible inputs**:
|
||||||
|
|
||||||
|
- The name to display in the merge request UI for the artifacts download link.
|
||||||
|
Must be combined with [`artifacts:paths`](#artifactspaths).
|
||||||
|
|
||||||
|
**Example of `artifacts:expose_as`**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
test:
|
test:
|
||||||
|
@ -756,108 +766,50 @@ test:
|
||||||
paths: ['file.txt']
|
paths: ['file.txt']
|
||||||
```
|
```
|
||||||
|
|
||||||
With this configuration, GitLab adds a link **artifact 1** to the relevant merge request
|
**Additional details**:
|
||||||
that points to `file1.txt`. To access the link, select **View exposed artifact**
|
|
||||||
below the pipeline graph in the merge request overview.
|
|
||||||
|
|
||||||
An example that matches an entire directory:
|
- If `artifacts:paths` uses [CI/CD variables](../variables/index.md), the artifacts do not display in the UI.
|
||||||
|
|
||||||
```yaml
|
|
||||||
test:
|
|
||||||
script: ["mkdir test && echo 'test' > test/file.txt"]
|
|
||||||
artifacts:
|
|
||||||
expose_as: 'artifact 1'
|
|
||||||
paths: ['test/']
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the following:
|
|
||||||
|
|
||||||
- Artifacts do not display in the merge request UI when using variables to define the `artifacts:paths`.
|
|
||||||
- A maximum of 10 job artifacts per merge request can be exposed.
|
- A maximum of 10 job artifacts per merge request can be exposed.
|
||||||
- Glob patterns are unsupported.
|
- Glob patterns are unsupported.
|
||||||
- If a directory is specified, the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts) if there is more than
|
- If a directory is specified and there is more than one file in the directory,
|
||||||
one file in the directory.
|
the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts).
|
||||||
- For exposed single file artifacts with `.html`, `.htm`, `.txt`, `.json`, `.xml`,
|
- If [GitLab Pages](../../administration/pages/index.md) is enabled, GitLab automatically
|
||||||
and `.log` extensions, if [GitLab Pages](../../administration/pages/index.md) is:
|
renders the artifacts when the artifacts is a single file with one of these extensions:
|
||||||
- Enabled, GitLab automatically renders the artifact.
|
- `.html` or `.htm`
|
||||||
- Not enabled, the file is displayed in the artifacts browser.
|
- `.txt`
|
||||||
|
- `.json`
|
||||||
|
- `.xml`
|
||||||
|
- `.log`
|
||||||
|
|
||||||
#### `artifacts:name`
|
#### `artifacts:name`
|
||||||
|
|
||||||
Use the `name` directive to define the name of the created artifacts
|
Use the `artifacts:name` keyword to define the name of the created artifacts
|
||||||
archive. You can specify a unique name for every archive. The `artifacts:name`
|
archive. You can specify a unique name for every archive.
|
||||||
variable can make use of any of the [predefined variables](../variables/index.md).
|
|
||||||
The default name is `artifacts`, which becomes `artifacts.zip` when you download it.
|
If not defined, the default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
|
||||||
|
|
||||||
|
**Keyword type**: Job keyword. You can use it only as part of a job or in the
|
||||||
|
[`default:` section](#default).
|
||||||
|
|
||||||
|
**Possible inputs**:
|
||||||
|
|
||||||
|
- The name of the artifacts archive. Can use [CI/CD variables](../variables/index.md).
|
||||||
|
|
||||||
|
**Example of `artifacts:name`**:
|
||||||
|
|
||||||
To create an archive with a name of the current job:
|
To create an archive with a name of the current job:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
job:
|
job:
|
||||||
artifacts:
|
artifacts:
|
||||||
name: "$CI_JOB_NAME"
|
name: "job1-artifacts-file"
|
||||||
paths:
|
paths:
|
||||||
- binaries/
|
- binaries/
|
||||||
```
|
```
|
||||||
|
|
||||||
To create an archive with a name of the current branch or tag including only
|
**Related topics**:
|
||||||
the binaries directory:
|
|
||||||
|
|
||||||
```yaml
|
- [Use CI/CD variables to define the artifacts name.](../pipelines/job_artifacts.md#use-cicd-variables-to-define-the-artifacts-name)
|
||||||
job:
|
|
||||||
artifacts:
|
|
||||||
name: "$CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
```
|
|
||||||
|
|
||||||
If your branch-name contains forward slashes
|
|
||||||
(for example `feature/my-feature`) it's advised to use `$CI_COMMIT_REF_SLUG`
|
|
||||||
instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
|
|
||||||
|
|
||||||
To create an archive with a name of the current job and the current branch or
|
|
||||||
tag including only the binaries directory:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
job:
|
|
||||||
artifacts:
|
|
||||||
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
```
|
|
||||||
|
|
||||||
To create an archive with a name of the current [stage](#stages) and branch name:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
job:
|
|
||||||
artifacts:
|
|
||||||
name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
If you use **Windows Batch** to run your shell scripts you must replace
|
|
||||||
`$` with `%`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
job:
|
|
||||||
artifacts:
|
|
||||||
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
|
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
```
|
|
||||||
|
|
||||||
If you use **Windows PowerShell** to run your shell scripts you must replace
|
|
||||||
`$` with `$env:`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
job:
|
|
||||||
artifacts:
|
|
||||||
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- binaries/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `artifacts:paths`
|
#### `artifacts:paths`
|
||||||
|
|
||||||
|
|
|
@ -3576,6 +3576,9 @@ msgstr ""
|
||||||
msgid "An error occurred fetching the project authors."
|
msgid "An error occurred fetching the project authors."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occurred fetching the public deploy keys. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred previewing the blob"
|
msgid "An error occurred previewing the blob"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23536,6 +23539,9 @@ msgstr ""
|
||||||
msgid "No projects found"
|
msgid "No projects found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No public deploy keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "No public groups"
|
msgid "No public groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import { GlTable, GlButton } from '@gitlab/ui';
|
import { GlLoadingIcon, GlEmptyState, GlPagination } from '@gitlab/ui';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import responseBody from 'test_fixtures/api/deploy_keys/index.json';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
|
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
|
||||||
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
|
||||||
|
jest.mock('~/api');
|
||||||
|
jest.mock('~/flash');
|
||||||
|
|
||||||
describe('DeployKeysTable', () => {
|
describe('DeployKeysTable', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -14,12 +23,53 @@ describe('DeployKeysTable', () => {
|
||||||
emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
|
emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deployKey = responseBody[0];
|
||||||
|
const deployKey2 = responseBody[1];
|
||||||
|
|
||||||
const createComponent = (provide = {}) => {
|
const createComponent = (provide = {}) => {
|
||||||
wrapper = mountExtended(DeployKeysTable, {
|
wrapper = mountExtended(DeployKeysTable, {
|
||||||
provide: merge({}, defaultProvide, provide),
|
provide: merge({}, defaultProvide, provide),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findEditButton = (index) =>
|
||||||
|
wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index);
|
||||||
|
const findRemoveButton = (index) =>
|
||||||
|
wrapper.findAllByLabelText(DeployKeysTable.i18n.remove, { selector: 'button' }).at(index);
|
||||||
|
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||||
|
const findTimeAgoTooltip = (index) => wrapper.findAllComponents(TimeAgoTooltip).at(index);
|
||||||
|
const findPagination = () => wrapper.findComponent(GlPagination);
|
||||||
|
|
||||||
|
const expectDeployKeyIsRendered = (expectedDeployKey, expectedRowIndex) => {
|
||||||
|
const editButton = findEditButton(expectedRowIndex);
|
||||||
|
const timeAgoTooltip = findTimeAgoTooltip(expectedRowIndex);
|
||||||
|
|
||||||
|
expect(wrapper.findByText(expectedDeployKey.title).exists()).toBe(true);
|
||||||
|
expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'code' }).exists()).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(timeAgoTooltip.exists()).toBe(true);
|
||||||
|
expect(timeAgoTooltip.props('time')).toBe(expectedDeployKey.created_at);
|
||||||
|
expect(editButton.exists()).toBe(true);
|
||||||
|
expect(editButton.attributes('href')).toBe(`/admin/deploy_keys/${expectedDeployKey.id}/edit`);
|
||||||
|
expect(findRemoveButton(expectedRowIndex).exists()).toBe(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const itRendersTheEmptyState = () => {
|
||||||
|
it('renders empty state', () => {
|
||||||
|
const emptyState = wrapper.findComponent(GlEmptyState);
|
||||||
|
|
||||||
|
expect(emptyState.exists()).toBe(true);
|
||||||
|
expect(emptyState.props()).toMatchObject({
|
||||||
|
svgPath: defaultProvide.emptyStateSvgPath,
|
||||||
|
title: DeployKeysTable.i18n.emptyStateTitle,
|
||||||
|
description: DeployKeysTable.i18n.emptyStateDescription,
|
||||||
|
primaryButtonText: DeployKeysTable.i18n.newDeployKeyButtonText,
|
||||||
|
primaryButtonLink: defaultProvide.createPath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
@ -30,18 +80,128 @@ describe('DeployKeysTable', () => {
|
||||||
expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true);
|
expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders table', () => {
|
|
||||||
createComponent();
|
|
||||||
|
|
||||||
expect(wrapper.findComponent(GlTable).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders `New deploy key` button', () => {
|
it('renders `New deploy key` button', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
const newDeployKeyButton = wrapper.findComponent(GlButton);
|
const newDeployKeyButton = wrapper.findByTestId('new-deploy-key-button');
|
||||||
|
|
||||||
expect(newDeployKeyButton.text()).toBe(DeployKeysTable.i18n.newDeployKeyButtonText);
|
expect(newDeployKeyButton.exists()).toBe(true);
|
||||||
expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath);
|
expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when `/deploy_keys` API request is pending', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Api.deployKeys.mockImplementation(() => new Promise(() => {}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows loading icon', async () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findLoadingIcon().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when `/deploy_keys` API request is successful', () => {
|
||||||
|
describe('when there are deploy keys', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Api.deployKeys.mockResolvedValue({
|
||||||
|
data: responseBody,
|
||||||
|
headers: { 'x-total': `${responseBody.length}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders deploy keys in table', () => {
|
||||||
|
expectDeployKeyIsRendered(deployKey, 0);
|
||||||
|
expectDeployKeyIsRendered(deployKey2, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pagination', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Api.deployKeys.mockResolvedValueOnce({
|
||||||
|
data: [deployKey],
|
||||||
|
headers: { 'x-total': '2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders pagination', () => {
|
||||||
|
const pagination = findPagination();
|
||||||
|
expect(pagination.exists()).toBe(true);
|
||||||
|
expect(pagination.props()).toMatchObject({
|
||||||
|
value: 1,
|
||||||
|
perPage: DEFAULT_PER_PAGE,
|
||||||
|
totalItems: responseBody.length,
|
||||||
|
nextText: DeployKeysTable.i18n.pagination.next,
|
||||||
|
prevText: DeployKeysTable.i18n.pagination.prev,
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when pagination is changed', () => {
|
||||||
|
it('calls API with `page` parameter', async () => {
|
||||||
|
const pagination = findPagination();
|
||||||
|
expectDeployKeyIsRendered(deployKey, 0);
|
||||||
|
|
||||||
|
Api.deployKeys.mockResolvedValue({
|
||||||
|
data: [deployKey2],
|
||||||
|
headers: { 'x-total': '2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
pagination.vm.$emit('input', 2);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findLoadingIcon().exists()).toBe(true);
|
||||||
|
expect(pagination.exists()).toBe(false);
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(Api.deployKeys).toHaveBeenCalledWith({
|
||||||
|
page: 2,
|
||||||
|
public: true,
|
||||||
|
});
|
||||||
|
expectDeployKeyIsRendered(deployKey2, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when there are no deploy keys', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Api.deployKeys.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
headers: { 'x-total': '0' },
|
||||||
|
});
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
itRendersTheEmptyState();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when `deploy_keys` API request is unsuccessful', () => {
|
||||||
|
const error = new Error('Network Error');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Api.deployKeys.mockRejectedValue(error);
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
itRendersTheEmptyState();
|
||||||
|
|
||||||
|
it('displays flash', () => {
|
||||||
|
expect(createFlash).toHaveBeenCalledWith({
|
||||||
|
message: DeployKeysTable.i18n.apiErrorMessage,
|
||||||
|
captureError: true,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Api from '~/api';
|
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import httpStatus from '~/lib/utils/http_status';
|
import httpStatus from '~/lib/utils/http_status';
|
||||||
|
|
||||||
|
@ -1574,6 +1574,51 @@ describe('Api', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deployKeys', () => {
|
||||||
|
it('fetches deploy keys', async () => {
|
||||||
|
const deployKeys = [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'My title 1',
|
||||||
|
created_at: '2021-10-29T16:59:55.229Z',
|
||||||
|
expires_at: null,
|
||||||
|
key:
|
||||||
|
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDLvQzRX960N7dxPdge9o5a96+M4GEGQ7rxT2D3wAQDtQFjQV5ZcKb5wfeLtYLe3kRVI4lCO10PXeQppb1XBaYmVO31IaRkcgmMEPVyfp76Dp4CJZz6aMEbbcqfaHkDre0Fa8kzTXnBJVh2NeDbBfGMjFM5NRQLhKykodNsepO6dQ== dummy@gitlab.com',
|
||||||
|
fingerprint: '81:93:63:b9:1e:24:a2:aa:e0:87:d3:3f:42:81:f2:c2',
|
||||||
|
projects_with_write_access: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
description: null,
|
||||||
|
name: 'project1',
|
||||||
|
name_with_namespace: 'John Doe3 / project1',
|
||||||
|
path: 'project1',
|
||||||
|
path_with_namespace: 'namespace1/project1',
|
||||||
|
created_at: '2021-10-29T16:59:54.668Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
description: null,
|
||||||
|
name: 'project2',
|
||||||
|
name_with_namespace: 'John Doe4 / project2',
|
||||||
|
path: 'project2',
|
||||||
|
path_with_namespace: 'namespace2/project2',
|
||||||
|
created_at: '2021-10-29T16:59:55.116Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/deploy_keys`;
|
||||||
|
mock.onGet(expectedUrl).reply(httpStatus.OK, deployKeys);
|
||||||
|
|
||||||
|
const params = { page: 2, public: true };
|
||||||
|
const { data } = await Api.deployKeys(params);
|
||||||
|
|
||||||
|
expect(data).toEqual(deployKeys);
|
||||||
|
expect(mock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Feature Flag User List', () => {
|
describe('Feature Flag User List', () => {
|
||||||
let expectedUrl;
|
let expectedUrl;
|
||||||
let projectId;
|
let projectId;
|
||||||
|
|
24
spec/frontend/fixtures/api_deploy_keys.rb
Normal file
24
spec/frontend/fixtures/api_deploy_keys.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
|
||||||
|
include ApiHelpers
|
||||||
|
include JavaScriptFixturesHelpers
|
||||||
|
|
||||||
|
let_it_be(:admin) { create(:admin) }
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:project2) { create(:project) }
|
||||||
|
let_it_be(:deploy_key) { create(:deploy_key, public: true) }
|
||||||
|
let_it_be(:deploy_key2) { create(:deploy_key, public: true) }
|
||||||
|
let_it_be(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key) }
|
||||||
|
let_it_be(:deploy_keys_project2) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key) }
|
||||||
|
let_it_be(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key2) }
|
||||||
|
let_it_be(:deploy_keys_project4) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key2) }
|
||||||
|
|
||||||
|
it 'api/deploy_keys/index.json' do
|
||||||
|
get api("/deploy_keys", admin)
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue