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)
|
||||
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
||||
- [ ] Quarantine test check-list
|
||||
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/guidelines/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).
|
||||
- [ ] 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/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).
|
||||
- [ ] (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
|
||||
- [ ] 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).
|
||||
- [ ] (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.
|
||||
|
||||
<!-- Base labels. -->
|
||||
|
|
|
@ -1,13 +1,28 @@
|
|||
<script>
|
||||
import { GlTable, GlButton } from '@gitlab/ui';
|
||||
import { GlTable, GlButton, GlPagination, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
|
||||
|
||||
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 {
|
||||
name: 'DeployKeysTable',
|
||||
i18n: {
|
||||
pageTitle: __('Public deploy keys'),
|
||||
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: [
|
||||
{
|
||||
|
@ -29,13 +44,83 @@ export default {
|
|||
{
|
||||
key: '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: {
|
||||
GlTable,
|
||||
GlButton,
|
||||
GlPagination,
|
||||
TimeAgoTooltip,
|
||||
GlLoadingIcon,
|
||||
GlEmptyState,
|
||||
},
|
||||
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>
|
||||
|
||||
|
@ -45,10 +130,71 @@ export default {
|
|||
<h4 class="gl-m-0">
|
||||
{{ $options.i18n.pageTitle }}
|
||||
</h4>
|
||||
<gl-button variant="confirm" :href="createPath">{{
|
||||
<gl-button variant="confirm" :href="createPath" data-testid="new-deploy-key-button">{{
|
||||
$options.i18n.newDeployKeyButtonText
|
||||
}}</gl-button>
|
||||
</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>
|
||||
</template>
|
||||
|
|
|
@ -91,6 +91,7 @@ const Api = {
|
|||
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
|
||||
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
|
||||
notificationSettingsPath: '/api/:version/notification_settings',
|
||||
deployKeysPath: '/api/:version/deploy_keys',
|
||||
|
||||
group(groupId, callback = () => {}) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
@ -950,6 +951,12 @@ const Api = {
|
|||
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 = {}) {
|
||||
let url = Api.buildUrl(this.notificationSettingsPath);
|
||||
|
||||
|
|
|
@ -388,7 +388,7 @@ export default {
|
|||
<div
|
||||
v-if="showMultiLineComment"
|
||||
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}')">
|
||||
<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).
|
||||
|
||||
### 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
|
||||
|
||||
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 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
|
||||
|
||||
To retrieve a job artifact from a different project, you might need to use a
|
||||
|
|
|
@ -628,12 +628,13 @@ test_job_2:
|
|||
|
||||
### `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).
|
||||
|
||||
The artifacts are sent to GitLab after the job finishes. They are
|
||||
available for download in the GitLab UI if the size is not
|
||||
larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
|
||||
available for download in the GitLab UI if the size is smaller than the
|
||||
the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
|
||||
|
||||
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
|
||||
|
@ -652,16 +653,18 @@ artifacts are restored after [caches](#cache).
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15122) in GitLab 13.1
|
||||
> - Requires GitLab Runner 13.1
|
||||
|
||||
`exclude` makes it possible to prevent files from being added to an artifacts
|
||||
archive.
|
||||
Use `artifacts:exclude` to prevent files from being added to an artifacts archive.
|
||||
|
||||
Similar to [`artifacts:paths`](#artifactspaths), `exclude` paths are 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.
|
||||
**Keyword type**: Job keyword. You can use it only as part of a job or in the
|
||||
[`default:` section](#default).
|
||||
|
||||
For example, to store all files in `binaries/`, but not `*.o` files located in
|
||||
subdirectories of `binaries/`:
|
||||
**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.
|
||||
|
||||
**Example of `artifacts:exclude`**:
|
||||
|
||||
```yaml
|
||||
artifacts:
|
||||
|
@ -671,20 +674,18 @@ artifacts:
|
|||
- 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:
|
||||
paths:
|
||||
- binaries/
|
||||
exclude:
|
||||
- binaries/temp/**/*
|
||||
```
|
||||
- `artifacts:exclude` paths are not searched recursively.
|
||||
- Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
|
||||
`artifacts:exclude` too.
|
||||
|
||||
Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
|
||||
`artifacts:exclude` too.
|
||||
**Related topics**:
|
||||
|
||||
- [Exclude files from job artifacts](../pipelines/job_artifacts.md#exclude-files-from-job-artifacts)
|
||||
|
||||
#### `artifacts:expire_in`
|
||||
|
||||
|
@ -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)
|
||||
for more information.
|
||||
|
||||
The value of `expire_in` is an elapsed time in seconds, unless a unit is provided. Valid values
|
||||
include:
|
||||
After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
|
||||
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 seconds`
|
||||
|
@ -717,7 +724,7 @@ include:
|
|||
- `3 weeks and 2 days`
|
||||
- `never`
|
||||
|
||||
To expire artifacts one week after being uploaded:
|
||||
**Example of `artifacts:expire_in`**:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
|
@ -725,28 +732,31 @@ job:
|
|||
expire_in: 1 week
|
||||
```
|
||||
|
||||
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)
|
||||
(30 days by default).
|
||||
**Additional details**:
|
||||
|
||||
To override the expiration date and protect artifacts from being automatically deleted:
|
||||
|
||||
- Select **Keep** on the job page.
|
||||
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
|
||||
`expire_in` to `never`.
|
||||
|
||||
After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
|
||||
accessible anymore.
|
||||
- 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.
|
||||
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
|
||||
`expire_in` to `never`.
|
||||
|
||||
#### `artifacts:expose_as`
|
||||
|
||||
> [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)
|
||||
in the [merge request](../../user/project/merge_requests/index.md) UI.
|
||||
Use the `artifacts:expose_as` keyword to
|
||||
[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
|
||||
test:
|
||||
|
@ -756,108 +766,50 @@ test:
|
|||
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.
|
||||
**Additional details**:
|
||||
|
||||
An example that matches an entire directory:
|
||||
|
||||
```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`.
|
||||
- If `artifacts:paths` uses [CI/CD variables](../variables/index.md), the artifacts do not display in the UI.
|
||||
- A maximum of 10 job artifacts per merge request can be exposed.
|
||||
- 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
|
||||
one file in the directory.
|
||||
- For exposed single file artifacts with `.html`, `.htm`, `.txt`, `.json`, `.xml`,
|
||||
and `.log` extensions, if [GitLab Pages](../../administration/pages/index.md) is:
|
||||
- Enabled, GitLab automatically renders the artifact.
|
||||
- Not enabled, the file is displayed in the artifacts browser.
|
||||
- If a directory is specified and there is more than one file in the directory,
|
||||
the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts).
|
||||
- If [GitLab Pages](../../administration/pages/index.md) is enabled, GitLab automatically
|
||||
renders the artifacts when the artifacts is a single file with one of these extensions:
|
||||
- `.html` or `.htm`
|
||||
- `.txt`
|
||||
- `.json`
|
||||
- `.xml`
|
||||
- `.log`
|
||||
|
||||
#### `artifacts:name`
|
||||
|
||||
Use the `name` directive to define the name of the created artifacts
|
||||
archive. You can specify a unique name for every archive. The `artifacts:name`
|
||||
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.
|
||||
Use the `artifacts:name` keyword to define the name of the created artifacts
|
||||
archive. You can specify a unique name for every archive.
|
||||
|
||||
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:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "$CI_JOB_NAME"
|
||||
name: "job1-artifacts-file"
|
||||
paths:
|
||||
- binaries/
|
||||
```
|
||||
|
||||
To create an archive with a name of the current branch or tag including only
|
||||
the binaries directory:
|
||||
**Related topics**:
|
||||
|
||||
```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](#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/
|
||||
```
|
||||
- [Use CI/CD variables to define the artifacts name.](../pipelines/job_artifacts.md#use-cicd-variables-to-define-the-artifacts-name)
|
||||
|
||||
#### `artifacts:paths`
|
||||
|
||||
|
|
|
@ -3576,6 +3576,9 @@ msgstr ""
|
|||
msgid "An error occurred fetching the project authors."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred fetching the public deploy keys. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred previewing the blob"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23536,6 +23539,9 @@ msgstr ""
|
|||
msgid "No projects found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No public deploy keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "No public groups"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
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 waitForPromises from 'helpers/wait_for_promises';
|
||||
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', () => {
|
||||
let wrapper;
|
||||
|
@ -14,12 +23,53 @@ describe('DeployKeysTable', () => {
|
|||
emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
|
||||
};
|
||||
|
||||
const deployKey = responseBody[0];
|
||||
const deployKey2 = responseBody[1];
|
||||
|
||||
const createComponent = (provide = {}) => {
|
||||
wrapper = mountExtended(DeployKeysTable, {
|
||||
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(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -30,18 +80,128 @@ describe('DeployKeysTable', () => {
|
|||
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', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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 Api from '~/api';
|
||||
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
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', () => {
|
||||
let expectedUrl;
|
||||
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