Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2023-07-29 00:10:22 +00:00
parent 0e2a219d6a
commit aa02d34e84
15 changed files with 675 additions and 65 deletions

View File

@ -232,10 +232,10 @@ export const getFilterTokens = (locationSearch) =>
};
});
const isNotEmptySearchToken = (token) =>
export const isNotEmptySearchToken = (token) =>
!(token.type === FILTERED_SEARCH_TERM && !token.value.data);
const isSpecialFilter = (type, data) => {
export const isSpecialFilter = (type, data) => {
const isAssigneeIdParam =
type === TOKEN_TYPE_ASSIGNEE &&
isPositiveInteger(data) &&

View File

@ -2,15 +2,38 @@
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
import { getParameterByName } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
import {
convertToSearchQuery,
convertToApiParams,
getInitialPageParams,
getFilterTokens,
isSortKey,
} from '~/issues/list/utils';
import {
OPERATORS_IS_NOT,
OPERATORS_IS_NOT_OR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
MAX_LIST_SIZE,
ISSUE_REFERENCE,
PARAM_STATE,
PARAM_FIRST_PAGE_SIZE,
PARAM_LAST_PAGE_SIZE,
PARAM_PAGE_AFTER,
PARAM_PAGE_BEFORE,
PARAM_SORT,
CREATED_DESC,
UPDATED_DESC,
urlSortParams,
} from '~/issues/list/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql';
@ -24,12 +47,12 @@ import {
noSearchNoFilterTitle,
searchPlaceholder,
SERVICE_DESK_BOT_USERNAME,
MAX_LIST_SIZE,
STATUS_OPEN,
STATUS_CLOSED,
STATUS_ALL,
WORKSPACE_PROJECT,
} from '../constants';
import { convertToUrlParams } from '../utils';
import {
searchWithinTokenBase,
assigneeTokenBase,
@ -68,6 +91,7 @@ export default {
'fullPath',
'isServiceDeskSupported',
'hasAnyIssues',
'initialSort',
],
props: {
eeSearchTokens: {
@ -81,7 +105,12 @@ export default {
serviceDeskIssues: [],
serviceDeskIssuesCounts: {},
sortOptions: [],
filterTokens: [],
pageInfo: {},
pageParams: {},
sortKey: CREATED_DESC,
state: STATUS_OPEN,
pageSize: DEFAULT_PAGE_SIZE,
issuesError: null,
};
},
@ -109,7 +138,7 @@ export default {
Sentry.captureException(error);
},
skip() {
return !this.hasAnyIssues;
return this.shouldSkipQuery;
},
},
serviceDeskIssuesCounts: {
@ -124,6 +153,9 @@ export default {
this.issuesError = this.$options.i18n.errorFetchingCounts;
Sentry.captureException(error);
},
skip() {
return this.shouldSkipQuery;
},
context: {
isSingleRequest: true,
},
@ -131,14 +163,23 @@ export default {
},
computed: {
queryVariables() {
const isIidSearch = ISSUE_REFERENCE.test(this.searchQuery);
return {
fullPath: this.fullPath,
iid: isIidSearch ? this.searchQuery.slice(1) : undefined,
isProject: this.isProject,
isSignedIn: this.isSignedIn,
authorUsername: SERVICE_DESK_BOT_USERNAME,
sort: this.sortKey,
state: this.state,
...this.pageParams,
...this.apiFilterParams,
search: isIidSearch ? undefined : this.searchQuery,
};
},
shouldSkipQuery() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts;
return {
@ -147,12 +188,40 @@ export default {
[STATUS_ALL]: allIssues?.count,
};
},
urlParams() {
return {
sort: urlSortParams[this.sortKey],
state: this.state,
...this.urlFilterParams,
first_page_size: this.pageParams.firstPageSize,
last_page_size: this.pageParams.lastPageSize,
page_after: this.pageParams.afterCursor ?? undefined,
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
isInfoBannerVisible() {
return this.isServiceDeskSupported && this.hasAnyIssues;
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
},
hasSearch() {
return Boolean(
this.searchQuery ||
Object.keys(this.urlFilterParams).length ||
this.pageParams.afterCursor ||
this.pageParams.beforeCursor,
);
},
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
urlFilterParams() {
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
searchTokens() {
const preloadedUsers = [];
@ -219,7 +288,15 @@ export default {
return tokens;
},
},
watch: {
$route(newValue, oldValue) {
if (newValue.fullPath !== oldValue.fullPath) {
this.updateData(getParameterByName(PARAM_SORT));
}
},
},
created() {
this.updateData(this.initialSort);
this.cache = {};
},
methods: {
@ -287,6 +364,37 @@ export default {
return;
}
this.state = state;
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
handleFilter(tokens) {
this.filterTokens = tokens;
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === STATUS_CLOSED ? UPDATED_DESC : CREATED_DESC;
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
const sortKey = graphQLSortKey || defaultSortKey;
this.filterTokens = getFilterTokens(window.location.search);
this.pageParams = getInitialPageParams(
this.pageSize,
isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
isPositiveInteger(lastPageSize) ? parseInt(lastPageSize, 10) : undefined,
getParameterByName(PARAM_PAGE_AFTER),
getParameterByName(PARAM_PAGE_BEFORE),
);
this.sortKey = sortKey;
this.state = state || STATUS_OPEN;
},
},
};
@ -297,16 +405,22 @@ export default {
<info-banner v-if="isInfoBannerVisible" />
<issuable-list
namespace="service-desk"
recent-searches-storage-key="issues"
recent-searches-storage-key="service-desk-issues"
:error="issuesError"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:initial-filter-value="filterTokens"
:show-filtered-search-friendly-text="hasOrFeature"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="serviceDeskIssues"
:tabs="$options.issuableListTabs"
:tab-counts="tabCounts"
:current-tab="state"
:default-page-size="pageSize"
sync-filter-and-sort
@click-tab="handleClickTab"
@filter="handleFilter"
>
<template #empty-state>
<gl-empty-state

View File

@ -1,12 +1,231 @@
import { __, s__ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_EPIC,
TOKEN_TYPE_HEALTH,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
ALTERNATIVE_FILTER,
API_PARAM,
NORMAL_FILTER,
SPECIAL_FILTER,
URL_PARAM,
} from '~/issues/list/constants';
export const SERVICE_DESK_BOT_USERNAME = 'support-bot';
export const MAX_LIST_SIZE = 10;
export const ISSUE_REFERENCE = /^#\d+$/;
export const STATUS_ALL = 'all';
export const STATUS_CLOSED = 'closed';
export const STATUS_OPEN = 'opened';
export const WORKSPACE_PROJECT = 'project';
export const filtersMap = {
[FILTERED_SEARCH_TERM]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'search',
},
[URL_PARAM]: {
[undefined]: {
[NORMAL_FILTER]: 'search',
},
},
},
[TOKEN_TYPE_SEARCH_WITHIN]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'in',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'in',
},
},
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsernames',
[SPECIAL_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username[]',
[SPECIAL_FILTER]: 'assignee_id',
[ALTERNATIVE_FILTER]: 'assignee_username',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
[OPERATOR_OR]: {
[NORMAL_FILTER]: 'or[assignee_username][]',
},
},
},
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'milestoneTitle',
[SPECIAL_FILTER]: 'milestoneWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone_title',
[SPECIAL_FILTER]: 'milestone_title',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
[SPECIAL_FILTER]: 'not[milestone_title]',
},
},
},
[TOKEN_TYPE_LABEL]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
[ALTERNATIVE_FILTER]: 'labelNames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
[ALTERNATIVE_FILTER]: 'label_name',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
[OPERATOR_OR]: {
[ALTERNATIVE_FILTER]: 'or[label_name][]',
},
},
},
[TOKEN_TYPE_TYPE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'types',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[type][]',
},
},
},
[TOKEN_TYPE_RELEASE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'releaseTag',
[SPECIAL_FILTER]: 'releaseTagWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'release_tag',
[SPECIAL_FILTER]: 'release_tag',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[release_tag]',
},
},
},
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'myReactionEmoji',
[SPECIAL_FILTER]: 'myReactionEmoji',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[my_reaction_emoji]',
},
},
},
[TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'confidential',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
},
},
[TOKEN_TYPE_ITERATION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'iterationId',
[SPECIAL_FILTER]: 'iterationWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]',
[SPECIAL_FILTER]: 'not[iteration_id]',
},
},
},
[TOKEN_TYPE_EPIC]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'epicId',
[SPECIAL_FILTER]: 'epicId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
},
},
[TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
},
},
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'healthStatusFilter',
[SPECIAL_FILTER]: 'healthStatusFilter',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
[SPECIAL_FILTER]: 'health_status',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[health_status]',
},
},
},
};
export const errorFetchingCounts = __('An error occurred while getting issue counts');
export const errorFetchingIssues = __('An error occurred while loading issues');
export const noSearchNoFilterTitle = __('Please select at least one filter to see results');

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue';
import { gqlClient } from './graphql';
@ -23,6 +24,7 @@ export async function mountServiceDeskListApp() {
projectDataIsProject,
projectDataIsSignedIn,
projectDataHasAnyIssues,
projectDataInitialSort,
serviceDeskEmailAddress,
canAdminIssues,
canEditProjectSettings,
@ -34,6 +36,7 @@ export async function mountServiceDeskListApp() {
} = el.dataset;
Vue.use(VueApollo);
Vue.use(VueRouter);
return new Vue({
el,
@ -41,6 +44,11 @@ export async function mountServiceDeskListApp() {
apolloProvider: new VueApollo({
defaultClient: await gqlClient(),
}),
router: new VueRouter({
base: window.location.pathname,
mode: 'history',
routes: [{ path: '/' }],
}),
provide: {
releasesPath: projectDataReleasesPath,
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
@ -61,6 +69,7 @@ export async function mountServiceDeskListApp() {
isServiceDeskSupported: parseBoolean(isServiceDeskSupported),
isServiceDeskEnabled: parseBoolean(isServiceDeskEnabled),
hasAnyIssues: parseBoolean(projectDataHasAnyIssues),
initialSort: projectDataInitialSort,
},
render: (createComponent) => createComponent(ServiceDeskListApp),
});

View File

@ -0,0 +1,37 @@
import {
OPERATOR_OR,
TOKEN_TYPE_LABEL,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { isSpecialFilter, isNotEmptySearchToken } from '~/issues/list/utils';
import {
ALTERNATIVE_FILTER,
NORMAL_FILTER,
SPECIAL_FILTER,
URL_PARAM,
} from '~/issues/list/constants';
import { filtersMap } from './constants';
const getFilterType = ({ type, value: { data, operator } }) => {
const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
if (isUnionedLabel) {
return ALTERNATIVE_FILTER;
}
if (isSpecialFilter(type, data)) {
return SPECIAL_FILTER;
}
return NORMAL_FILTER;
};
export const convertToUrlParams = (filterTokens) => {
const urlParamsMap = filterTokens.filter(isNotEmptySearchToken).reduce((acc, token) => {
const filterType = getFilterType(token);
const urlParam = filtersMap[token.type][URL_PARAM][token.value.operator]?.[filterType];
return acc.set(
urlParam,
acc.has(urlParam) ? [acc.get(urlParam), token.value.data].flat() : token.value.data,
);
}, new Map());
return Object.fromEntries(urlParamsMap);
};

View File

@ -41,7 +41,7 @@ To enable the agent server on a single node:
1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
For additional configuration options, see the **Enable GitLab KAS** section of the
[`gitlab.rb.template`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/files/gitlab-config-template/gitlab.rb.template).
[`gitlab.rb.template`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/be52c36c243a3422ec38b7d45d459682a07e195f/files/gitlab-config-template/gitlab.rb.template#L1951).
##### Configure KAS to listen on a UNIX socket

View File

@ -236,37 +236,61 @@ The project for a new Gem should always be created in [`gitlab-org/ruby/gems` na
1. Determine a suitable name for the gem. If it's a GitLab-owned gem, prefix
the gem name with `gitlab-`. For example, `gitlab-sidekiq-fetcher`.
1. Create the gem or fork as necessary.
1. Ensure the `gitlab_rubygems` group is an owner of the new gem by running:
1. Locally create the gem or fork as necessary.
1. [Publish an empty `0.0.1` version of the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg) to ensure the gem name is reserved.
1. Add the [`gitlab_rubygems`](https://rubygems.org/profiles/gitlab_rubygems) and [`gitlab-qa`](https://rubygems.org/profiles/gitlab-qa) users as owners of the new gem by running:
```shell
gem owner <gem-name> --add gitlab_rubygems
gem owner <gem-name> --add gitlab-qa
```
1. [Publish the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg)
1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem published
successfully and `gitlab_rubygems` is also an owner.
1. Create a project in [`gitlab-org/ruby/gems` namespace](https://gitlab.com/gitlab-org/ruby/gems/).
1. Optional. Add some or all of the following users as co-owners:
- [Marin Jankovski](https://rubygems.org/profiles/marinjankovski)
- [Rémy Coutable](https://rubygems.org/profiles/rymai)
- [Stan Hu](https://rubygems.org/profiles/stanhu)
1. Optional. Add any other relevant developers as co-owners.
1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem was published
successfully and `gitlab_rubygems` & `gitlab-qa` are also owners.
1. Create a project in the [`gitlab-org/ruby/gems` group](https://gitlab.com/gitlab-org/ruby/gems/). To create this project:
1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project).
1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration).
1. Use the [shared CI/CD config](https://gitlab.com/gitlab-org/quality/pipeline-common/-/blob/master/ci/gem-release.yml)
to release and publish new gem versions by adding the following to their `.gitlab-ci.yml`:
- To create this project:
1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project).
1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration).
1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#publishing-a-project).
- See [issue #325463](https://gitlab.com/gitlab-org/gitlab/-/issues/325463)
for an example.
- In some cases we may want to move a gem to its own namespace. Some
examples might be that it will naturally have more than one project
(say, something that has plugins as separate libraries), or that we
expect users outside GitLab to be maintainers on this project as
well as GitLab team members.
```yaml
include:
- project: 'gitlab-org/quality/pipeline-common'
file: '/ci/gem-release.yml'
```
The latter situation (maintainers from outside GitLab) could also
apply if someone who currently works at GitLab wants to maintain
the gem beyond their time working at GitLab.
This job will handle building and publishing the gem (it uses a `gilab-qa` Rubygems.org
API token inherited from the `gitlab-org/ruby/gems` group, in order to publish the gem
package), as well as creating the tag, release and populating its release notes by
using the
[Generate changelog data](../api/repositories.md#generate-changelog-data)
API endpoint.
When publishing a gem to RubyGems.org, also note the section on
[gem owners](https://about.gitlab.com/handbook/developer-onboarding/#ruby-gems)
in the handbook.
For instructions for when and how to generate a changelog entry file, see the
dedicated [Changelog entries](changelog.md)
page.
[To be consistent with the GitLab project](changelog.md),
Gem projects could also define a changelog YAML configuration file at
`.gitlab/changelog_config.yml` with the same content
as [in the `gitlab-styles` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/blob/master/.gitlab/changelog_config.yml).
1. To ease the release process, you could also create a `.gitlab/merge_request_templates/Release.md` MR template with the same content
as [in the `gitlab-styles` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/raw/master/.gitlab/merge_request_templates/Release.md)
(make sure to replace `gitlab-styles` with the actual gem name).
1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#publishing-a-project).
Notes: In some cases we may want to move a gem to its own namespace. Some
examples might be that it will naturally have more than one project
(say, something that has plugins as separate libraries), or that we
expect users outside GitLab to be maintainers on this project as
well as GitLab team members.
The latter situation (maintainers from outside GitLab) could also
apply if someone who currently works at GitLab wants to maintain
the gem beyond their time working at GitLab.
## The `vendor/gems/`

View File

@ -1,14 +1,16 @@
---
stage: Deploy
group: Environments
info: A tutorial for structuring a repository for GitOps deployments
info: A tutorial for deploying a GitLab repository using Flux
---
# Tutorial: Structure your repository for GitOps deployments **(FREE)**
# Tutorial: Deploy a Git repository using Flux **(FREE)**
In this tutorial, you'll create a GitLab project that builds and deploys an application
to a Kubernetes cluster using Flux. You'll set up a sample manifest project, configure it to
push manifests to a deployment branch, and configure Flux to sync the deployment branch.
push manifests to a deployment branch, and configure Flux to sync the deployment branch. With this
setup, you can run additional steps in GitLab pipelines before Flux picks up the changes
from the repository.
This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).

View File

@ -8,7 +8,9 @@ info: A tutorial for deploying an OCI artifact using Flux
This tutorial teaches you how to package your Kubernetes manifests into an [OCI](https://opencontainers.org/)
artifact and deploy them to your cluster using Flux. You'll set up a sample manifest project, configure it to
store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact.
store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact. With this
setup, you can run additional steps in GitLab pipelines before Flux picks up the changes
from the OCI image.
This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).

View File

@ -124,7 +124,7 @@
"clipboard": "^2.0.8",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.31.1",
"core-js": "^3.32.0",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
@ -202,8 +202,8 @@
"tippy.js": "^6.3.7",
"traverse": "^0.6.7",
"unified": "^10.1.2",
"unist-builder": "^3.0.1",
"unist-util-visit-parents": "^5.1.3",
"unist-builder": "^4.0.0",
"unist-util-visit-parents": "5.1.3",
"url-loader": "^4.1.1",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",

View File

@ -2,14 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
import VueRouter from 'vue-router';
import * as Sentry from '@sentry/browser';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { STATUS_CLOSED, STATUS_OPEN } from '~/service_desk/constants';
import { STATUS_CLOSED, STATUS_OPEN, STATUS_ALL } from '~/service_desk/constants';
import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
@ -27,14 +30,19 @@ import {
import {
getServiceDeskIssuesQueryResponse,
getServiceDeskIssuesCountsQueryResponse,
filteredTokens,
urlParams,
locationSearch,
} from '../mock_data';
jest.mock('@sentry/browser');
describe('ServiceDeskListApp', () => {
describe('CE ServiceDeskListApp', () => {
let wrapper;
let router;
Vue.use(VueApollo);
Vue.use(VueRouter);
const defaultProvide = {
releasesPath: 'releases/path',
@ -49,6 +57,7 @@ describe('ServiceDeskListApp', () => {
fullPath: 'path/to/project',
isServiceDeskSupported: true,
hasAnyIssues: true,
initialSort: '',
};
let defaultQueryResponse = getServiceDeskIssuesQueryResponse;
@ -82,6 +91,8 @@ describe('ServiceDeskListApp', () => {
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
];
router = new VueRouter({ mode: 'history' });
return shallowMount(ServiceDeskListApp, {
apolloProvider: createMockApollo(
requestHandlers,
@ -98,6 +109,7 @@ describe('ServiceDeskListApp', () => {
},
},
),
router,
provide: {
...defaultProvide,
...provide,
@ -106,6 +118,7 @@ describe('ServiceDeskListApp', () => {
};
beforeEach(() => {
setWindowLocation(TEST_HOST);
wrapper = createComponent();
return waitForPromises();
});
@ -113,7 +126,7 @@ describe('ServiceDeskListApp', () => {
it('fetches service desk issues and renders them in the issuable list', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: 'service-desk',
recentSearchesStorageKey: 'issues',
recentSearchesStorageKey: 'service-desk-issues',
issuables: defaultQueryResponse.data.project.issues.nodes,
tabs: issuableListTabs,
currentTab: STATUS_OPEN,
@ -145,6 +158,36 @@ describe('ServiceDeskListApp', () => {
});
});
describe('Initial url params', () => {
describe('search', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
wrapper = createComponent();
expect(router.history.current.query).toMatchObject({ search: 'find issues' });
});
});
describe('state', () => {
it('is set from the url params', () => {
const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
wrapper = createComponent();
expect(findIssuableList().props('currentTab')).toBe(initialState);
});
});
describe('filter tokens', () => {
it('are set from the url params', () => {
setWindowLocation(locationSearch);
wrapper = createComponent();
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
});
});
});
describe('Tokens', () => {
const mockCurrentUser = {
id: 1,
@ -200,14 +243,36 @@ describe('ServiceDeskListApp', () => {
describe('Events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
it('updates ui to the new tab', async () => {
createComponent();
beforeEach(() => {
wrapper = createComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
await nextTick();
it('updates ui to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
it('updates url to the new tab', () => {
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining({ state: STATUS_CLOSED }),
});
});
});
describe('when "filter" event is emitted by IssuableList', () => {
it('updates IssuableList with url params', async () => {
wrapper = createComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('filter', filteredTokens);
await nextTick();
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining(urlParams),
});
});
});
});

View File

@ -1,3 +1,20 @@
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_EPIC,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getServiceDeskIssuesQueryResponse = {
data: {
project: {
@ -116,3 +133,104 @@ export const getServiceDeskIssuesCountsQueryResponse = {
},
},
};
export const filteredTokens = [
{ type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
];
export const urlParams = {
search: 'find issues',
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
'or[assignee_username][]': ['carl', 'lenny'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
'or[label_name][]': ['comedy', 'sitcom'],
release_tag: ['v3', 'v4'],
'not[release_tag]': ['v20', 'v30'],
my_reaction_emoji: 'thumbsup',
'not[my_reaction_emoji]': 'thumbsdown',
confidential: 'yes',
iteration_id: ['4', '12'],
'not[iteration_id]': ['20', '42'],
epic_id: '12',
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
health_status: 'atRisk',
'not[health_status]': 'onTrack',
};
export const locationSearch = [
'?search=find+issues',
'assignee_username[]=bart',
'assignee_username[]=lisa',
'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
'or[assignee_username][]=carl',
'or[assignee_username][]=lenny',
'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
'not[milestone_title]=season+30',
'label_name[]=cartoon',
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
'or[label_name][]=comedy',
'or[label_name][]=sitcom',
'release_tag=v3',
'release_tag=v4',
'not[release_tag]=v20',
'not[release_tag]=v30',
'my_reaction_emoji=thumbsup',
'not[my_reaction_emoji]=thumbsdown',
'confidential=yes',
'iteration_id=4',
'iteration_id=12',
'not[iteration_id]=20',
'not[iteration_id]=42',
'epic_id=12',
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
'health_status=atRisk',
'not[health_status]=onTrack',
].join('&');

View File

@ -23,10 +23,12 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
describe '#add_comment_for_added_validations' do
let(:file_lines) { file_diff.map { |line| line.delete_prefix('+').delete_prefix('-') } }
let(:filename) { 'app/models/user.rb' }
let(:added_filename) { 'app/models/user.rb' }
before do
allow(model_validations.project_helper).to receive(:file_lines).and_return(file_lines)
allow(model_validations.helper).to receive(:all_changed_files).and_return([filename])
allow(model_validations.helper).to receive(:added_files).and_return([added_filename])
allow(model_validations.helper).to receive(:modified_files).and_return([filename])
allow(model_validations.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
end
@ -83,11 +85,13 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
app/models/users/user_follow_user.rb
ee/app/models/ee/user.rb
ee/app/models/sca/license_policy.rb
app/models/concerns/presentable.rb
]
end
before do
all_new_files = %w[
added_files = %w[app/models/user_preferences.rb app/models/concerns/presentable.rb]
modified_files = %w[
app/models/user.rb
app/models/users/user_follow_user.rb
ee/app/models/ee/user.rb
@ -96,7 +100,8 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
app/assets/index.js
]
allow(model_validations.helper).to receive(:all_changed_files).and_return(all_new_files)
allow(model_validations.helper).to receive(:added_files).and_return(added_files)
allow(model_validations.helper).to receive(:modified_files).and_return(modified_files)
end
it 'returns added and modified files' do

View File

@ -7,8 +7,11 @@ module Tooling
module ModelValidations
include ::Tooling::Danger::Suggestor
MODEL_FILES_REGEX = 'app/models'
MODEL_FILES_PATH = 'app/models'
MODEL_CONCERN_FILES_PATH = 'app/models/concerns'
EE_PREFIX = 'ee/'
MODEL_FILES_REGEX = %r{\A(#{EE_PREFIX})?#{MODEL_FILES_PATH}}
MODEL_CONCERN_FILES_REGEX = %r{\A(#{EE_PREFIX})?#{MODEL_CONCERN_FILES_PATH}}
VALIDATION_METHODS = %w[validates validate validates_each validates_with validates_associated].freeze
VALIDATIONS_REGEX = /^\+\s*(.*\.)?(#{VALIDATION_METHODS.join('|')})[( ]/
@ -32,10 +35,10 @@ module Tooling
end
def changed_model_files
changed_files = helper.all_changed_files
ee_folder_prefix = "(#{EE_PREFIX})?"
added_files = helper.added_files
modified_files = helper.modified_files
changed_files.grep(%r{\A#{ee_folder_prefix}#{MODEL_FILES_REGEX}})
modified_files.grep(MODEL_FILES_REGEX) + added_files.grep(MODEL_CONCERN_FILES_REGEX)
end
end
end

View File

@ -2478,7 +2478,12 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/unist@*", "@types/unist@^2.0.0":
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a"
integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==
"@types/unist@^2.0.0":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
@ -4267,10 +4272,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^3.29.1, core-js@^3.31.1, core-js@^3.6.5:
version "3.31.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.1.tgz#f2b0eea9be9da0def2c5fece71064a7e5d687653"
integrity sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==
core-js@^3.29.1, core-js@^3.32.0, core-js@^3.6.5:
version "3.32.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6"
integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==
core-util-is@~1.0.0:
version "1.0.3"
@ -12554,13 +12559,20 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
unist-builder@^3.0.0, unist-builder@^3.0.1:
unist-builder@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.1.tgz#258b89dcadd3c973656b2327b347863556907f58"
integrity sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==
dependencies:
"@types/unist" "^2.0.0"
unist-builder@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-4.0.0.tgz#817b326c015a6f9f5e92bb55b8e8bc5e578fe243"
integrity sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-generated@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
@ -12585,18 +12597,18 @@ unist-util-stringify-position@^3.0.0:
dependencies:
"@types/unist" "^2.0.0"
unist-util-visit-parents@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==
unist-util-visit-parents@5.1.3, unist-util-visit-parents@^5.0.0:
version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
unist-util-visit-parents@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"