Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-04 15:10:31 +00:00
parent 557b3c487b
commit 333d6f857e
31 changed files with 439 additions and 313 deletions

View File

@ -1,15 +1,14 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertEnvironmentScope } from '../utils';
export default {
name: 'CiEnvironmentsDropdown',
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlDropdownItem,
GlCollapsibleListbox,
},
props: {
environments: {
@ -24,6 +23,7 @@ export default {
},
data() {
return {
selectedEnvironment: '',
searchTerm: '',
};
},
@ -33,9 +33,15 @@ export default {
},
filteredEnvironments() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.environments.filter((environment) => {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
});
return this.environments
.filter((environment) => {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
})
.map((environment) => ({
value: environment,
text: environment,
}));
},
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);
@ -47,44 +53,29 @@ export default {
methods: {
selectEnvironment(selected) {
this.$emit('select-environment', selected);
this.clearSearch();
},
convertEnvironmentScopeValue(scope) {
return convertEnvironmentScope(scope);
this.selectedEnvironment = selected;
},
createEnvironmentScope() {
this.$emit('create-environment-scope', this.searchTerm);
this.selectEnvironment(this.searchTerm);
},
isSelected(env) {
return this.selectedEnvironmentScope === env;
},
clearSearch() {
this.searchTerm = '';
},
},
};
</script>
<template>
<gl-dropdown :text="environmentScopeLabel" @show="clearSearch">
<gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
<gl-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment"
:is-checked="isSelected(environment)"
is-check-item
@click="selectEnvironment(environment)"
>
{{ convertEnvironmentScopeValue(environment) }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!filteredEnvironments.length" ref="noMatchingResults">{{
__('No matching results')
}}</gl-dropdown-item>
<template v-if="shouldRenderCreateButton">
<gl-collapsible-listbox
v-model="selectedEnvironment"
searchable
:items="filteredEnvironments"
:toggle-text="environmentScopeLabel"
@search="searchTerm = $event.trim()"
@select="selectEnvironment"
>
<template v-if="shouldRenderCreateButton" #footer>
<gl-dropdown-divider />
<gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope">
{{ composedCreateButtonLabel }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</gl-collapsible-listbox>
</template>

View File

@ -352,7 +352,6 @@ export default {
</template>
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
class="gl-w-full"
:selected-environment-scope="variable.environmentScope"
:environments="joinedEnvironments"
@select-environment="setEnvironmentScope"

View File

@ -288,7 +288,7 @@ export default {
</div>
</gl-form-group>
<div>
<gl-button variant="success" @click="createDeployToken">
<gl-button variant="confirm" @click="createDeployToken">
{{ $options.translations.addTokenButton }}
</gl-button>
</div>

View File

@ -2,7 +2,7 @@
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { MESSAGE_EVENT_TYPE, OBSERVABILITY_ROUTES, SKELETON_VARIANT } from '../constants';
import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '../constants';
import ObservabilitySkeleton from './skeleton/index.vue';
export default {
@ -23,16 +23,16 @@ export default {
);
},
getSkeletonVariant() {
switch (this.$route.path) {
case OBSERVABILITY_ROUTES.DASHBOARDS:
return SKELETON_VARIANT.DASHBOARDS;
case OBSERVABILITY_ROUTES.EXPLORE:
return SKELETON_VARIANT.EXPLORE;
case OBSERVABILITY_ROUTES.MANAGE:
return SKELETON_VARIANT.MANAGE;
default:
return SKELETON_VARIANT.DASHBOARDS;
}
const [, variant] =
Object.entries(SKELETON_VARIANTS_BY_ROUTE).find(([path]) =>
this.$route.path.endsWith(path),
) || [];
const DEFAULT_SKELETON = 'dashboards';
if (!variant) return DEFAULT_SKELETON;
return variant;
},
},
mounted() {
@ -51,7 +51,7 @@ export default {
} = e;
switch (type) {
case MESSAGE_EVENT_TYPE.GOUI_LOADED:
this.$refs.iframeSkeleton.handleSkeleton();
this.$refs.observabilitySkeleton.onContentLoaded();
break;
case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
this.routeUpdateHandler(payload);
@ -80,7 +80,7 @@ export default {
</script>
<template>
<observability-skeleton ref="iframeSkeleton" :variant="getSkeletonVariant">
<observability-skeleton ref="observabilitySkeleton" :variant="getSkeletonVariant">
<iframe
id="observability-ui-iframe"
data-testid="observability-ui-iframe"

View File

@ -1,17 +1,32 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { SKELETON_VARIANT } from '../../constants';
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import {
SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
DEFAULT_TIMERS,
OBSERVABILITY_ROUTES,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
} from '../../constants';
import DashboardsSkeleton from './dashboards.vue';
import ExploreSkeleton from './explore.vue';
import ManageSkeleton from './manage.vue';
export default {
SKELETON_VARIANT,
components: {
GlSkeletonLoader,
DashboardsSkeleton,
ExploreSkeleton,
ManageSkeleton,
GlAlert,
},
SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
OBSERVABILITY_ROUTES,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
},
props: {
variant: {
@ -22,65 +37,94 @@ export default {
},
data() {
return {
loading: null,
timerId: null,
state: null,
loadingTimeout: null,
errorTimeout: null,
};
},
mounted() {
this.timerId = setTimeout(() => {
/**
* If observability UI is not loaded then this.loading would be null
* we will show skeleton in that case
*/
if (this.loading !== false) {
this.showSkeleton();
}
}, 500);
this.setLoadingTimeout();
this.setErrorTimeout();
},
destroyed() {
clearTimeout(this.loadingTimeout);
clearTimeout(this.errorTimeout);
},
methods: {
handleSkeleton() {
if (this.loading === null) {
/**
* If observability UI content loads with in 500ms
* do not show skeleton.
*/
clearTimeout(this.timerId);
return;
}
onContentLoaded() {
clearTimeout(this.errorTimeout);
clearTimeout(this.loadingTimeout);
/**
* If observability UI content loads after 500ms
* wait for 400ms to hide skeleton.
* This is mostly to avoid the flashing effect If content loads imediately after skeleton
*/
setTimeout(this.hideSkeleton, 400);
this.hideSkeleton();
},
setLoadingTimeout() {
this.loadingTimeout = setTimeout(() => {
/**
* If content is not loaded within CONTENT_WAIT_MS,
* show the skeleton
*/
if (this.state !== SKELETON_STATE.HIDDEN) {
this.showSkeleton();
}
}, DEFAULT_TIMERS.CONTENT_WAIT_MS);
},
setErrorTimeout() {
this.errorTimeout = setTimeout(() => {
/**
* If content is not loaded within TIMEOUT_MS,
* show the error dialog
*/
if (this.state !== SKELETON_STATE.HIDDEN) {
this.showError();
}
}, DEFAULT_TIMERS.TIMEOUT_MS);
},
hideSkeleton() {
this.loading = false;
this.state = SKELETON_STATE.HIDDEN;
},
showSkeleton() {
this.loading = true;
this.state = SKELETON_STATE.VISIBLE;
},
showError() {
this.state = SKELETON_STATE.ERROR;
},
isSkeletonShown(route) {
return this.variant === SKELETON_VARIANTS_BY_ROUTE[route];
},
},
};
</script>
<template>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
<div v-show="loading" class="gl-px-5">
<dashboards-skeleton v-if="variant === $options.SKELETON_VARIANT.DASHBOARDS" />
<explore-skeleton v-else-if="variant === $options.SKELETON_VARIANT.EXPLORE" />
<manage-skeleton v-else-if="variant === $options.SKELETON_VARIANT.MANAGE" />
<transition name="fade">
<div v-if="state === $options.SKELETON_STATE.VISIBLE" class="gl-px-5">
<dashboards-skeleton v-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.DASHBOARDS)" />
<explore-skeleton v-else-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.EXPLORE)" />
<manage-skeleton v-else-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.MANAGE)" />
<gl-skeleton-loader v-else>
<rect y="2" width="10" height="8" />
<rect y="2" x="15" width="15" height="8" />
<rect y="2" x="35" width="15" height="8" />
<rect y="15" width="400" height="30" />
</gl-skeleton-loader>
</div>
<gl-skeleton-loader v-else>
<rect y="2" width="10" height="8" />
<rect y="2" x="15" width="15" height="8" />
<rect y="2" x="35" width="15" height="8" />
<rect y="15" width="400" height="30" />
</gl-skeleton-loader>
</div>
</transition>
<gl-alert
v-if="state === $options.SKELETON_STATE.ERROR"
:title="$options.i18n.TIMEOUT_ERROR_LABEL"
variant="danger"
:dismissible="false"
class="gl-m-5"
>
{{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
</gl-alert>
<div
v-show="!loading"
v-show="state === $options.SKELETON_STATE.HIDDEN"
data-testid="observability-wrapper"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
>
<slot></slot>

View File

@ -1,16 +1,32 @@
import { __ } from '~/locale';
export const MESSAGE_EVENT_TYPE = Object.freeze({
GOUI_LOADED: 'GOUI_LOADED',
GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
});
export const OBSERVABILITY_ROUTES = Object.freeze({
DASHBOARDS: '/groups/gitlab-org/-/observability/dashboards',
EXPLORE: '/groups/gitlab-org/-/observability/explore',
MANAGE: '/groups/gitlab-org/-/observability/manage',
DASHBOARDS: 'observability/dashboards',
EXPLORE: 'observability/explore',
MANAGE: 'observability/manage',
});
export const SKELETON_VARIANT = Object.freeze({
DASHBOARDS: 'dashboards',
EXPLORE: 'explore',
MANAGE: 'manage',
export const SKELETON_VARIANTS_BY_ROUTE = Object.freeze({
[OBSERVABILITY_ROUTES.DASHBOARDS]: 'dashboards',
[OBSERVABILITY_ROUTES.EXPLORE]: 'explore',
[OBSERVABILITY_ROUTES.MANAGE]: 'manage',
});
export const SKELETON_STATE = Object.freeze({
ERROR: 'error',
VISIBLE: 'visible',
HIDDEN: 'hidden',
});
export const DEFAULT_TIMERS = Object.freeze({
TIMEOUT_MS: 20000,
CONTENT_WAIT_MS: 500,
});
export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');

View File

@ -183,6 +183,8 @@ module ApplicationHelper
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
return "" if time.nil?
css_classes = [short_format ? 'js-short-timeago' : 'js-timeago']
css_classes << html_class unless html_class.blank?

View File

@ -919,8 +919,12 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
next variables unless tag?
git_tag = project.repository.find_tag(ref)
next variables unless git_tag
variables.append(key: 'CI_COMMIT_TAG', value: ref)
variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: project.repository.find_tag(ref).message)
variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: git_tag.message)
# legacy variable
variables.append(key: 'CI_BUILD_TAG', value: ref)

View File

@ -59,7 +59,6 @@ month. Major API version changes, and removal of entire API versions, are done i
with major GitLab releases.
All deprecations and changes between versions are in the documentation.
For the changes between v3 and v4, see the [v3 to v4 documentation](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/11-0-stable/doc/api/v3_to_v4.md).
### Current status

View File

@ -1,11 +0,0 @@
---
redirect_to: 'https://gitlab.com/gitlab-org/gitlab-foss/-/blob/11-0-stable/doc/api/v3_to_v4.md'
remove_date: '2023-01-31'
---
This document was moved to [another location](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/11-0-stable/doc/api/v3_to_v4.md).
<!-- This redirect file can be deleted after <2023-01-31>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -148,26 +148,25 @@ In addition, we should add the following columns to `ci_runners`:
future uses that may not be apparent.
```sql
CREATE TABLE ci_runner (
CREATE TABLE ci_runners (
...
creator_id bigint
registration_type int8
)
```
The `ci_builds_runner_session` (or `ci_builds` or `ci_builds_metadata`) shall reference
`ci_runner_machines`.
The `ci_builds_metadata` table shall reference `ci_runner_machines`.
We might consider a more efficient way to store `contacted_at` than updating the existing record.
```sql
CREATE TABLE ci_builds_runner_session (
CREATE TABLE ci_builds_metadata (
...
runner_machine_id bigint NOT NULL
);
CREATE TABLE ci_runner_machines (
id integer NOT NULL,
machine_id character varying UNIQUE NOT NULL,
id bigint NOT NULL,
machine_xid character varying UNIQUE NOT NULL,
contacted_at timestamp without time zone,
version character varying,
revision character varying,
@ -241,7 +240,7 @@ future after the legacy registration system is removed, and runners have been up
versions.
Job pings from such legacy runners results in a `ci_runner_machines` record containing a
`<legacy>` `machine_id` field value.
`<legacy>` `machine_xid` field value.
Not using the unique system ID means that all connected runners with the same token are
notified, instead of just the runner matching the exact system identifier. While not ideal, this is
@ -320,9 +319,9 @@ using PAT tokens for example - such that every runner is associated with an owne
|------------------|----------:|---------|
| GitLab Rails app | | Create database migration to add columns to `ci_runners` table. |
| GitLab Rails app | | Create database migration to add `ci_runner_machines` table. |
| GitLab Rails app | | Create database migration to add `ci_runner_machines.machine_id` foreign key to `ci_builds_runner_session` table. |
| GitLab Rails app | | Create database migration to add `ci_runner_machines.id` foreign key to `ci_builds_metadata` table. |
| GitLab Rails app | | Create database migrations to add `allow_runner_registration_token` setting to `application_settings` and `namespace_settings` tables (default: `true`). |
| GitLab Runner | | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
| GitLab Rails app | | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
| GitLab Runner | | Start sending `system_id` value in `POST /jobs/request` request and other follow-up requests that require identifying the unique system. |
| GitLab Rails app | | Create service similar to `StaleGroupRunnersPruneCronWorker` service to clean up `ci_runner_machines` records instead of `ci_runners` records.<br/>Existing service continues to exist but focuses only on legacy runners. |

View File

@ -534,10 +534,6 @@ Also in the example, `GIT_STRATEGY` is set to `none`. If the
`stop_review_app` job is [automatically triggered](../environments/index.md#stop-an-environment),
the runner won't try to check out the code after the branch is deleted.
The example also overwrites global variables. If your `stop` `environment` job depends
on global variables, use [anchor variables](../yaml/yaml_optimization.md#yaml-anchors-for-variables) when you set the `GIT_STRATEGY`
to change the job without overriding the global variables.
The `stop_review_app` job **must** have the following keywords defined:
- `when`, defined at either:

View File

@ -4343,7 +4343,6 @@ deploy_review_job:
**Related topics**:
- You can use [YAML anchors for variables](yaml_optimization.md#yaml-anchors-for-variables).
- [Predefined variables](../variables/predefined_variables.md) are variables the runner
automatically creates and makes available in the job.
- You can [configure runner behavior with variables](../runners/configure_runners.md#configure-runner-behavior-with-variables).

View File

@ -189,30 +189,6 @@ job2:
- *some-script-after
```
### YAML anchors for variables
Use [YAML anchors](#anchors) with `variables` to repeat assignment
of variables across multiple jobs. You can also use YAML anchors when a job
requires a specific `variables` block that would otherwise override the global variables.
The following example shows how override the `GIT_STRATEGY` variable without affecting
the use of the `SAMPLE_VARIABLE` variable:
```yaml
# global variables
variables: &global-variables
SAMPLE_VARIABLE: sample_variable_value
ANOTHER_SAMPLE_VARIABLE: another_sample_variable_value
# a job that must set the GIT_STRATEGY variable, yet depend on global variables
job_no_git_strategy:
stage: cleanup
variables:
<<: *global-variables
GIT_STRATEGY: none
script: echo $SAMPLE_VARIABLE
```
## Use `extends` to reuse configuration sections
You can use the [`extends` keyword](index.md#extends) to reuse configuration in

View File

@ -294,16 +294,14 @@ end
### Verify the MR was deployed and the index exists in production
You can verify if the post-deploy migration was executed on GitLab.com by:
- Executing `/chatops run auto_deploy status <merge_sha>`. If the output returns `db/gprd`,
the post-deploy migration has been executed in the production database. More details in this
[guide](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/post_deploy_migration/readme.md#how-to-determine-if-a-post-deploy-migration-has-been-executed-on-gitlabcom).
- Use a meta-command in #database-lab, such as: `\d <index_name>`.
- Ensure that the index is not [`invalid`](https://www.postgresql.org/docs/12/sql-createindex.html#:~:text=The%20psql%20%5Cd%20command%20will%20report%20such%20an%20index%20as%20INVALID).
- Ask someone in #database to check if the index exists.
- With proper access, you can also verify directly on production or in a
production clone.
1. Verify that the post-deploy migration was executed on GitLab.com using ChatOps with
`/chatops run auto_deploy status <merge_sha>`. If the output returns `db/gprd`,
the post-deploy migration has been executed in the production database. For more information, see
[How to determine if a post-deploy migration has been executed on GitLab.com](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/post_deploy_migration/readme.md#how-to-determine-if-a-post-deploy-migration-has-been-executed-on-gitlabcom).
1. In the case of an [index created asynchronously](#schedule-the-index-to-be-created), wait
until the next week so that the index can be created over a weekend.
1. Use [Database Lab](database_lab.md) to check [if creation was successful](database_lab.md#checking-indexes).
Ensure the output does not indicate the index is `invalid`.
### Add a migration to create the index synchronously
@ -394,15 +392,15 @@ You must test the database index changes locally before creating a merge request
### Verify the MR was deployed and the index no longer exists in production
You can verify if the MR was deployed to GitLab.com with
`/chatops run auto_deploy status <merge_sha>`. To verify the existence of
the index, you can:
- Use a meta-command in `#database-lab`, for example: `\d <index_name>`.
- Make sure the index no longer exists
- Ask someone in `#database` to check if the index exists.
- If you have access, you can verify directly on production or in a
production clone.
1. Verify that the post-deploy migration was executed on GitLab.com using ChatOps with
`/chatops run auto_deploy status <merge_sha>`. If the output returns `db/gprd`,
the post-deploy migration has been executed in the production database. For more information, see
[How to determine if a post-deploy migration has been executed on GitLab.com](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/post_deploy_migration/readme.md#how-to-determine-if-a-post-deploy-migration-has-been-executed-on-gitlabcom).
1. In the case of an [index removed asynchronously](#schedule-the-index-to-be-removed), wait
until the next week so that the index can be created over a weekend.
1. Use Database Lab [to check if removal was successful](database_lab.md#checking-indexes).
[Database Lab](database_lab.md)
should report an error when trying to find the removed index. If not, the index may still exist.
### Add a migration to destroy the index synchronously

View File

@ -75,6 +75,45 @@ the new index. `exec` does not return any results, only the time required to exe
After many changes, such as after a destructive query or an ineffective index,
you must start over. To reset your designated clone, run `reset`.
#### Checking indexes
Use Database Lab to check the status of an index with the meta-command `\d <index_name>`.
Caveats:
- Indexes are created in both the `main` and `ci` databases, so be sure to use the instance
that matches the table's `gitlab_schema`. For example, if the index is added to
[`ci_builds`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/docs/ci_builds.yml#L14),
use `gitlab-production-ci`.
- Database Lab typically has a small delay of a few hours. If more up-to-date information
is required, you can instead request access to a replica [via Teleport](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/Teleport/Connect_to_Database_Console_via_Teleport.md)
For example: `\d index_design_management_designs_on_project_id` produces:
```plaintext
Index "public.index_design_management_designs_on_project_id"
Column | Type | Key? | Definition
------------+---------+------+------------
project_id | integer | yes | project_id
btree, for table "public.design_management_designs"
```
In the case of an invalid index, the output ends with `invalid`, like:
```plaintext
Index "public.index_design_management_designs_on_project_id"
Column | Type | Key? | Definition
------------+---------+------+------------
project_id | integer | yes | project_id
btree, for table "public.design_management_designs", invalid
```
If the index doesn't exist, JoeBot throws an error like:
```plaintext
ERROR: psql error: psql:/tmp/psql-query-932227396:1: error: Did not find any relation named "no_index".
```
### Migration testing
For information on testing migrations, review our

View File

@ -66,16 +66,16 @@ In the hierarchy list, public groups with a private subgroup have an expand opti
for all users that indicate there is a subgroup. When users who are not direct or inherited members of
the private subgroup select expand (**{chevron-down}**), the nested subgroup does not display.
If you prefer to keep information about the presence of nested subgroups private, we advise that you only
add private subgroups to private parent groups.
If you prefer to keep information about the presence of nested subgroups private, we advise that you
add private subgroups only to private parent groups.
## Create a subgroup
Prerequisites:
- You must either:
- Have at least the Maintainer role for a group to create subgroups for it.
- Have the [role determined by a setting](#change-who-can-create-subgroups). These users can create
- You must have either:
- At least the Maintainer role for a group to create subgroups for it.
- The [role determined by a setting](#change-who-can-create-subgroups). These users can create
subgroups even if group creation is
[disabled by an Administrator](../../admin_area/index.md#prevent-a-user-from-creating-groups) in the user's settings.
@ -92,8 +92,9 @@ To create a subgroup:
### Change who can create subgroups
To create a subgroup, you must have at least the Maintainer role on the group, depending on the group's setting. By
default:
Prerequisite:
- You must have at least the Maintainer role on the group, depending on the group's setting.
To change who can create subgroups on a group:
@ -120,11 +121,11 @@ There is a bug that causes some pages in the parent group to be accessible by su
When you add a member to a group, that member is also added to all subgroups. The user's permissions are inherited from
the group's parent.
Subgroup members can:
Subgroup members can be:
1. Be [direct members](../../project/members/index.md#add-users-to-a-project) of the subgroup.
1. [Inherit membership](../../project/members/index.md#inherited-membership) of the subgroup from the subgroup's parent group.
1. Be a member of a group that was [shared with the subgroup's top-level group](../manage.md#share-a-group-with-another-group).
1. [Direct members](../../project/members/index.md#add-users-to-a-project) of the subgroup.
1. [Inherited members](../../project/members/index.md#inherited-membership) of the subgroup from the subgroup's parent group.
1. Members of a group that was [shared with the subgroup's top-level group](../manage.md#share-a-group-with-another-group).
```mermaid
flowchart RL

View File

@ -1362,8 +1362,10 @@ Payload example:
## Job events
- Number of retries [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/382046) in GitLab 15.6 [with a flag](../../../administration/feature_flags.md)
- Number of retries (`retries_count`) [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/382046) in GitLab 15.6 [with a flag](../../../administration/feature_flags.md)
named `job_webhook_retries_count`. Disabled by default.
- Pipeline name (`commit.name`) [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107963) in GitLab 15.8 [with a flag](../../../administration/feature_flags.md)
named `pipeline_name`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
@ -1415,6 +1417,7 @@ Payload example:
},
"commit": {
"id": 2366,
"name": "Build pipeline",
"sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
"message": "test\n",
"author_name": "User",

View File

@ -70,6 +70,7 @@ module Gitlab
}
data[:retries_count] = build.retries_count if Feature.enabled?(:job_webhook_retries_count, project)
data[:commit][:name] = commit.name if Feature.enabled?(:pipeline_name, project)
data
end

View File

@ -34567,6 +34567,9 @@ msgstr ""
msgid "Reload page"
msgstr ""
msgid "Reload the page to try again."
msgstr ""
msgid "Remediations"
msgstr ""
@ -44255,6 +44258,9 @@ msgstr ""
msgid "Unable to load the merge request widget. Try reloading the page."
msgstr ""
msgid "Unable to load the page"
msgstr ""
msgid "Unable to parse JSON"
msgstr ""

View File

@ -57,7 +57,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.0.1",
"@gitlab/svgs": "3.14.0",
"@gitlab/ui": "52.7.0",
"@gitlab/ui": "52.7.2",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230102181448",
"@rails/actioncable": "6.1.4-7",

View File

@ -24,8 +24,9 @@ RSpec.describe 'Project variables', :js, feature_category: :pipeline_authoring d
page.within('#add-ci-variable') do
fill_in 'Key', with: 'akey'
find('#ci-variable-value').set('akey_value')
find('[data-testid="environment-scope"]').click
find('[data-testid="ci-environment-search"]').set('review/*')
click_button('All (default)')
fill_in 'Search', with: 'review/*'
find('[data-testid="create-wildcard-button"]').click
click_button('Add variable')

View File

@ -1,6 +1,5 @@
import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui';
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { allEnvironments } from '~/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
@ -10,11 +9,12 @@ describe('Ci environments dropdown', () => {
const envs = ['dev', 'prod', 'staging'];
const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
const findDropdownText = () => wrapper.findComponent(GlDropdown).text();
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
wrapper = mount(CiEnvironmentsDropdown, {
@ -24,7 +24,7 @@ describe('Ci environments dropdown', () => {
},
});
findSearchBox().vm.$emit('input', searchTerm);
findListbox().vm.$emit('search', searchTerm);
};
afterEach(() => {
@ -37,12 +37,9 @@ describe('Ci environments dropdown', () => {
});
it('renders create button with search term if environments do not contain search term', () => {
expect(findAllDropdownItems()).toHaveLength(2);
expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
});
it('renders empty results message', () => {
expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
const button = findCreateWildcardButton();
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Create wildcard: stable');
});
});
@ -52,13 +49,12 @@ describe('Ci environments dropdown', () => {
});
it('renders all environments when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe(envs[0]);
expect(findDropdownItemByIndex(1).text()).toBe(envs[1]);
expect(findDropdownItemByIndex(2).text()).toBe(envs[2]);
expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
});
it('should not display active checkmark on the inactive stage', () => {
it('does not display active checkmark on the inactive stage', () => {
expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
});
});
@ -71,53 +67,37 @@ describe('Ci environments dropdown', () => {
});
it('shows the `All environments` text and not the wildcard', () => {
expect(findDropdownText()).toContain(allEnvironments.text);
expect(findDropdownText()).not.toContain(wildcardScope);
expect(findListboxText()).toContain(allEnvironments.text);
expect(findListboxText()).not.toContain(wildcardScope);
});
});
describe('Environments found', () => {
const currentEnv = envs[2];
beforeEach(async () => {
beforeEach(() => {
createComponent({ searchTerm: currentEnv });
await nextTick();
});
it('renders only the environment searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe(currentEnv);
expect(findAllListboxItems()).toHaveLength(1);
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
});
it('should not display create button', () => {
const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
expect(environments).toHaveLength(0);
expect(findAllDropdownItems()).toHaveLength(1);
});
it('should not display empty results message', () => {
expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
});
it('should clear the search term when showing the dropdown', () => {
wrapper.findComponent(GlDropdown).trigger('click');
expect(findSearchBox().text()).toBe('');
it('does not display create button', () => {
expect(findCreateWildcardButton().exists()).toBe(false);
});
describe('Custom events', () => {
describe('when clicking on an environment', () => {
describe('when selecting an environment', () => {
const itemIndex = 0;
beforeEach(() => {
createComponent();
});
it('should emit `select-environment` if an environment is clicked', async () => {
await nextTick();
await findDropdownItemByIndex(itemIndex).vm.$emit('click');
it('emits `select-environment` when an environment is clicked', () => {
findListbox().vm.$emit('select', envs[itemIndex]);
expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
});
});
@ -128,9 +108,8 @@ describe('Ci environments dropdown', () => {
createComponent({ searchTerm: search });
});
it('should emit createClicked if an environment is clicked', async () => {
await nextTick();
findDropdownItemByIndex(1).vm.$emit('click');
it('emits create-environment-scope', () => {
findCreateWildcardButton().vm.$emit('click');
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
});

View File

@ -24,6 +24,7 @@ exports[`Design management design version dropdown component renders design vers
tabindex="-1"
>
<gl-listbox-item-stub
data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
ischeckcentered="true"
>
<span
@ -66,6 +67,7 @@ exports[`Design management design version dropdown component renders design vers
</span>
</gl-listbox-item-stub>
<gl-listbox-item-stub
data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
ischeckcentered="true"
>
<span
@ -142,6 +144,7 @@ exports[`Design management design version dropdown component renders design vers
tabindex="-1"
>
<gl-listbox-item-stub
data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
ischeckcentered="true"
>
<span
@ -184,6 +187,7 @@ exports[`Design management design version dropdown component renders design vers
</span>
</gl-listbox-item-stub>
<gl-listbox-item-stub
data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
ischeckcentered="true"
>
<span

View File

@ -2,11 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
import {
MESSAGE_EVENT_TYPE,
OBSERVABILITY_ROUTES,
SKELETON_VARIANT,
} from '~/observability/constants';
import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
import { darkModeEnabled } from '~/lib/utils/color_utils';
@ -20,6 +16,7 @@ describe('Observability root app', () => {
};
const $route = {
pathname: 'https://gitlab.com/gitlab-org/',
path: 'https://gitlab.com/gitlab-org/-/observability/dashboards',
query: { otherQuery: 100 },
};
@ -29,6 +26,10 @@ describe('Observability root app', () => {
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
const mountComponent = (route = $route) => {
wrapper = shallowMountExtended(ObservabilityApp, {
propsData: {
@ -139,9 +140,9 @@ describe('Observability root app', () => {
describe('on GOUI_LOADED', () => {
beforeEach(() => {
mountComponent();
wrapper.vm.$refs.iframeSkeleton.handleSkeleton = mockHandleSkeleton;
wrapper.vm.$refs.observabilitySkeleton.onContentLoaded = mockHandleSkeleton;
});
it('should call handleSkeleton method', () => {
it('should call onContentLoaded method', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://observe.gitlab.com',
@ -149,7 +150,7 @@ describe('Observability root app', () => {
expect(mockHandleSkeleton).toHaveBeenCalled();
});
it('should not call handleSkeleton method if origin is different', () => {
it('should not call onContentLoaded method if origin is different', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://example.com',
@ -157,7 +158,7 @@ describe('Observability root app', () => {
expect(mockHandleSkeleton).not.toHaveBeenCalled();
});
it('should not call handleSkeleton method if event type is different', () => {
it('should not call onContentLoaded method if event type is different', () => {
dispatchMessageEvent({
data: { type: 'UNKNOWN_EVENT' },
origin: 'https://observe.gitlab.com',
@ -168,11 +169,11 @@ describe('Observability root app', () => {
describe('skeleton variant', () => {
it.each`
pathDescription | path | variant
${'dashboards'} | ${OBSERVABILITY_ROUTES.DASHBOARDS} | ${SKELETON_VARIANT.DASHBOARDS}
${'explore'} | ${OBSERVABILITY_ROUTES.EXPLORE} | ${SKELETON_VARIANT.EXPLORE}
${'manage dashboards'} | ${OBSERVABILITY_ROUTES.MANAGE} | ${SKELETON_VARIANT.MANAGE}
${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANT.DASHBOARDS}
pathDescription | path | variant
${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
`('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => {
mountComponent({ ...$route, path });
const props = wrapper.findComponent(ObservabilitySkeleton).props();

View File

@ -1,96 +1,127 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { nextTick } from 'vue';
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
import Skeleton from '~/observability/components/skeleton/index.vue';
import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
import { SKELETON_VARIANT } from '~/observability/constants';
import { SKELETON_VARIANTS_BY_ROUTE, DEFAULT_TIMERS } from '~/observability/constants';
describe('ObservabilitySkeleton component', () => {
describe('Skeleton component', () => {
let wrapper;
const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
const findContentWrapper = () => wrapper.findByTestId('observability-wrapper');
const findExploreSkeleton = () => wrapper.findComponent(ExploreSkeleton);
const findDashboardsSkeleton = () => wrapper.findComponent(DashboardsSkeleton);
const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton);
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
wrapper = shallowMountExtended(ObservabilitySkeleton, {
wrapper = shallowMountExtended(Skeleton, {
propsData: props,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('on mount', () => {
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
mountComponent();
mountComponent({ variant: 'explore' });
});
it('should call setTimeout on mount and show ObservabilitySkeleton if Observability UI is not loaded yet', () => {
jest.runAllTimers();
describe('loading timers', () => {
it('show Skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
expect(wrapper.vm.loading).toBe(true);
expect(wrapper.vm.timerId).not.toBeNull();
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
expect(findExploreSkeleton().exists()).toBe(true);
expect(findContentWrapper().isVisible()).toBe(false);
});
it('does not show the skeleton if content has loaded within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
wrapper.vm.onContentLoaded();
await nextTick();
expect(findContentWrapper().isVisible()).toBe(true);
expect(findExploreSkeleton().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
expect(findContentWrapper().isVisible()).toBe(true);
expect(findExploreSkeleton().exists()).toBe(false);
});
});
it('should call setTimeout on mount and dont show ObservabilitySkeleton if Observability UI is loaded', () => {
wrapper.vm.loading = false;
jest.runAllTimers();
describe('error timeout', () => {
it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
expect(findAlert().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
expect(wrapper.vm.loading).toBe(false);
expect(wrapper.vm.timerId).not.toBeNull();
});
});
await nextTick();
describe('handleSkeleton', () => {
it('will not show the skeleton if Observability UI is loaded before', () => {
jest.spyOn(global, 'clearTimeout');
mountComponent();
wrapper.vm.handleSkeleton();
expect(clearTimeout).toHaveBeenCalledWith(wrapper.vm.timerId);
});
expect(findAlert().exists()).toBe(true);
expect(findContentWrapper().isVisible()).toBe(false);
});
it('will hide skeleton gracefully after 400ms if skeleton was present on screen before Observability UI', () => {
jest.spyOn(global, 'setTimeout');
mountComponent();
jest.runAllTimers();
wrapper.vm.handleSkeleton();
jest.runAllTimers();
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
wrapper.vm.onContentLoaded();
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
expect(setTimeout).toHaveBeenCalledWith(wrapper.vm.hideSkeleton, 400);
expect(wrapper.vm.loading).toBe(false);
await nextTick();
expect(findAlert().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(true);
});
});
});
describe('skeleton variant', () => {
it.each`
skeletonType | condition | variant
${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANT.DASHBOARDS}
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANT.EXPLORE}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANT.MANAGE}
${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]}
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
const showsDefaultSkeleton = ![
SKELETON_VARIANT.DASHBOARDS,
SKELETON_VARIANT.EXPLORE,
SKELETON_VARIANT.MANAGE,
].includes(variant);
expect(wrapper.findComponent(DashboardsSkeleton).exists()).toBe(
skeletonType === SKELETON_VARIANT.DASHBOARDS,
);
expect(wrapper.findComponent(ExploreSkeleton).exists()).toBe(
skeletonType === SKELETON_VARIANT.EXPLORE,
);
expect(wrapper.findComponent(ManageSkeleton).exists()).toBe(
skeletonType === SKELETON_VARIANT.MANAGE,
);
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
const showsDefaultSkeleton = !SKELETON_VARIANTS.includes(variant);
expect(findDashboardsSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[0]);
expect(findExploreSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[1]);
expect(findManageSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[2]);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
});
});
describe('on destroy', () => {
it('should clear init timer and timeout timer', () => {
jest.spyOn(global, 'clearTimeout');
mountComponent();
wrapper.destroy();
expect(clearTimeout).toHaveBeenCalledTimes(2);
expect(clearTimeout.mock.calls).toEqual([
[wrapper.vm.loadingTimeout], // First call
[wrapper.vm.errorTimeout], // Second call
]);
});
});
});

View File

@ -57,18 +57,23 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
tabindex="-1"
>
<gl-listbox-item-stub
data-testid="listbox-item-0"
isselected="true"
>
rspec
</gl-listbox-item-stub>
<gl-listbox-item-stub>
<gl-listbox-item-stub
data-testid="listbox-item-1"
>
cypress
</gl-listbox-item-stub>
<gl-listbox-item-stub>
<gl-listbox-item-stub
data-testid="listbox-item-2"
>
karma

View File

@ -163,6 +163,13 @@ RSpec.describe ApplicationHelper do
expect(timeago_element.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil
end
it 'returns blank if time is nil' do
el = helper.time_ago_with_tooltip(nil)
expect(el).to eq('')
expect(el.html_safe).to eq('')
end
end
describe '#active_when' do

View File

@ -2,10 +2,11 @@
require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Build do
RSpec.describe Gitlab::DataBuilder::Build, feature_category: :integrations do
let_it_be(:runner) { create(:ci_runner, :instance, :tagged_only) }
let_it_be(:user) { create(:user, :public_email) }
let_it_be(:ci_build) { create(:ci_build, :running, runner: runner, user: user) }
let_it_be(:pipeline) { create(:ci_pipeline, name: 'Build pipeline') }
let_it_be(:ci_build) { create(:ci_build, :running, pipeline: pipeline, runner: runner, user: user) }
describe '.build' do
around do |example|
@ -33,6 +34,7 @@ RSpec.describe Gitlab::DataBuilder::Build do
it { expect(data[:project_name]).to eq(ci_build.project.full_name) }
it { expect(data[:pipeline_id]).to eq(ci_build.pipeline.id) }
it { expect(data[:retries_count]).to eq(ci_build.retries_count) }
it { expect(data[:commit][:name]).to eq(pipeline.name) }
it {
expect(data[:user]).to eq(
@ -61,10 +63,10 @@ RSpec.describe Gitlab::DataBuilder::Build do
described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
end
expect(control.count).to eq(13)
expect(control.count).to eq(14)
end
context 'when feature flag is disabled' do
context 'when job_webhook_retries_count feature flag is disabled' do
before do
stub_feature_flags(job_webhook_retries_count: false)
end
@ -79,7 +81,26 @@ RSpec.describe Gitlab::DataBuilder::Build do
described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
end
expect(control.count).to eq(12)
expect(control.count).to eq(13)
end
end
context 'when pipeline_name feature flag is disabled' do
before do
stub_feature_flags(pipeline_name: false)
ci_build # Make sure the Ci::Build model is created before recording.
end
it { expect(data[:commit]).not_to have_key(:name) }
it 'does not exceed number of expected queries' do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
b = Ci::Build.find(ci_build.id)
described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
end
expect(control.count).to eq(13)
end
end

View File

@ -1322,6 +1322,21 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
context 'when tag is not found' do
let(:pipeline) do
create(:ci_pipeline, project: project, ref: 'not_found_tag', tag: true)
end
it 'does not expose tag variables' do
expect(subject.to_hash.keys)
.not_to include(
'CI_COMMIT_TAG',
'CI_COMMIT_TAG_MESSAGE',
'CI_BUILD_TAG'
)
end
end
context 'without a commit' do
let(:pipeline) { build(:ci_empty_pipeline, :created, sha: nil) }

View File

@ -1136,10 +1136,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.14.0.tgz#b32a673f08bbd5ba6d406bcf3abb6e7276271b6c"
integrity sha512-mQYtW9eGHY7cF6elsWd76hUF7F3NznyzrJJy5eXBHjvRdYBtyHmwkVmh1Cwr3S/2Sl8fPC+qk41a+Nm6n+1mRQ==
"@gitlab/ui@52.7.0":
version "52.7.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.7.0.tgz#431502f0b23324d57fa2f683925f17eb4c550f99"
integrity sha512-ttTCUt/amTMG9YhHJypR3FIFVSBjYDGh56izAhjMts7r216saaD8hQ8m4Yzts+fqmR42bQzUdjdThwj3Dthtsw==
"@gitlab/ui@52.7.2":
version "52.7.2"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.7.2.tgz#91d5709dbd964c3bc9af48f2a4bad6b69e37ecb0"
integrity sha512-YGg6UoaqNJ2OG5BdJewb5ov58GHaQjjCoNuRpwKuiXu+gE90yt8brRl2jYQTthU0lxS2UtHveU22KcP6ZbZJ6w==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"