From 1cd1c93a9e1d95663e51f4de24a5b518fa2bee1a Mon Sep 17 00:00:00 2001 From: ArnaudTa <33383276+ArnaudTA@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:42:18 +0200 Subject: [PATCH] feat: :sparkles: allow repo sync from console ui --- .../cypress/components/specs/repo-form.ct.ts | 2 +- apps/client/cypress/e2e/specs/repos.e2e.ts | 55 +++++++++++++- apps/client/cypress/e2e/support/commands.ts | 2 +- apps/client/src/api/repositories.ts | 7 +- apps/client/src/components/RepoForm.vue | 8 +-- apps/client/src/stores/project-repository.ts | 8 ++- apps/client/src/views/projects/DsoRepos.vue | 71 ++++++++++++++++--- apps/server/.env.integ-example | 1 + .../src/resources/repository/business.ts | 27 ++++++- .../resources/repository/controllers.spec.ts | 32 +++++++++ .../src/resources/repository/queries.ts | 13 ++++ .../server/src/resources/repository/router.ts | 24 +++++-- apps/server/src/utils/hook-wrapper.ts | 16 +++-- apps/server/src/utils/mocks.ts | 3 +- packages/hooks/src/hooks/hook-misc.ts | 5 +- packages/shared/src/contracts/repository.ts | 12 ++++ packages/shared/src/schemas/repository.ts | 17 +++++ packages/shared/tsconfig.json | 3 +- plugins/gitlab/src/class.ts | 55 +++++++++++--- plugins/gitlab/src/functions.ts | 24 ++++++- plugins/gitlab/src/index.ts | 13 +++- 21 files changed, 352 insertions(+), 46 deletions(-) diff --git a/apps/client/cypress/components/specs/repo-form.ct.ts b/apps/client/cypress/components/specs/repo-form.ct.ts index 08f7368bc..8e2a32397 100644 --- a/apps/client/cypress/components/specs/repo-form.ct.ts +++ b/apps/client/cypress/components/specs/repo-form.ct.ts @@ -115,7 +115,7 @@ describe('RepoForm.vue', () => { cy.mount(RepoForm, { props }) - cy.get('h1').should('contain', 'Modifier le dépôt') + cy.get('h2').should('contain', 'Modifier le dépôt') cy.getByDataTestid('repoFieldset').should('have.length', 1) cy.getByDataTestid('internalRepoNameInput').find('input').should('have.value', props.repo.internalRepoName) .and('be.disabled') diff --git a/apps/client/cypress/e2e/specs/repos.e2e.ts b/apps/client/cypress/e2e/specs/repos.e2e.ts index 6d0186b99..e56236c6b 100644 --- a/apps/client/cypress/e2e/specs/repos.e2e.ts +++ b/apps/client/cypress/e2e/specs/repos.e2e.ts @@ -28,7 +28,7 @@ describe('Add repos into project', () => { cy.url().should('contain', '/repositories') cy.getByDataTestid('addRepoLink').click({ timeout: 30_000 }) - cy.get('h1').should('contain', 'Ajouter un dépôt au projet') + cy.get('h2').should('contain', 'Ajouter un dépôt au projet') cy.getByDataTestid('addRepoBtn').should('be.disabled') cy.getByDataTestid('internalRepoNameInput').find('input').clear().type(repo.internalRepoName) cy.getByDataTestid('addRepoBtn').should('be.disabled') @@ -139,7 +139,7 @@ describe('Add repos into project', () => { cy.wait('@getProjects').its('response').then(response => { repos = response?.body.find(resProject => resProject.name === project.name).repositories cy.getByDataTestid(`repoTile-${repos[0].internalRepoName}`).click() - .get('h1').should('contain', 'Modifier le dépôt') + .get('h2').should('contain', 'Modifier le dépôt') .getByDataTestid('internalRepoNameInput').should('be.disabled') .getByDataTestid('externalRepoUrlInput').find('input').clear().type('https://github.com/externalUser04/new-repo.git') @@ -162,6 +162,57 @@ describe('Add repos into project', () => { }) }) + it('Should synchronise a repo', () => { + cy.intercept('GET', '/api/v1/projects').as('getProjects') + cy.intercept('GET', '/api/v1/projects/*/repositories/*/sync/*').as('syncRepo') + let repos + + cy.goToProjects() + .getByDataTestid(`projectTile-${project.name}`).click() + .getByDataTestid('menuRepos').click() + .url().should('contain', '/repositories') + + cy.wait('@getProjects').its('response').then(response => { + repos = response?.body.find(resProject => resProject.name === project.name).repositories + + cy.getByDataTestid(`repoTile-${repos[0].internalRepoName}`) + .click() + + cy.get('h2').should('contain', 'Synchroniser le dépôt') + cy.getByDataTestid('branchNameInput') + .should('have.value', 'main') + + cy.getByDataTestid('syncRepoBtn') + .should('be.enabled') + .click() + + cy.wait('@syncRepo').its('response.statusCode').should('match', /^20\d$/) + + cy.getByDataTestid('snackbar').within(() => { + cy.get('p').should('contain', `Dépôt ${repos[0].internalRepoName} synchronisé`) + }) + + cy.getByDataTestid('branchNameInput') + .clear() + + cy.getByDataTestid('syncRepoBtn') + .should('be.disabled') + + cy.getByDataTestid('branchNameInput') + .type('develop') + + cy.getByDataTestid('syncRepoBtn') + .should('be.enabled') + .click() + + cy.wait('@syncRepo').its('response.statusCode').should('match', /^20\d$/) + + cy.getByDataTestid('snackbar').within(() => { + cy.get('p').should('contain', `Dépôt ${repos[0].internalRepoName} synchronisé`) + }) + }) + }) + it('Should generate a GitLab CI for a repo', () => { cy.intercept('POST', '/api/v1/projects/*/repositories').as('postRepo') cy.intercept('GET', '/api/v1/projects').as('getProjects') diff --git a/apps/client/cypress/e2e/support/commands.ts b/apps/client/cypress/e2e/support/commands.ts index b1d1211a9..06bc509c0 100644 --- a/apps/client/cypress/e2e/support/commands.ts +++ b/apps/client/cypress/e2e/support/commands.ts @@ -116,7 +116,7 @@ Cypress.Commands.add('addRepos', (project, repos) => { newRepos.forEach((repo) => { cy.getByDataTestid('addRepoLink').click() - .get('h1').should('contain', 'Ajouter un dépôt au projet') + .get('h2').should('contain', 'Ajouter un dépôt au projet') .getByDataTestid('internalRepoNameInput').find('input').type(repo.internalRepoName) .getByDataTestid('externalRepoUrlInput').find('input').clear().type(repo.externalRepoUrl) diff --git a/apps/client/src/api/repositories.ts b/apps/client/src/api/repositories.ts index c9afb90e1..840a90808 100644 --- a/apps/client/src/api/repositories.ts +++ b/apps/client/src/api/repositories.ts @@ -1,4 +1,4 @@ -import type { CreateRepositoryBody, UpdateRepositoryBody, RepositoryParams } from '@cpn-console/shared' +import type { CreateRepositoryBody, UpdateRepositoryBody, SyncRepositoryParams, RepositoryParams } from '@cpn-console/shared' import { apiClient } from './xhr-client.js' export const addRepo = async (projectId: RepositoryParams['projectId'], data: CreateRepositoryBody) => { @@ -11,6 +11,11 @@ export const getRepos = async (projectId: RepositoryParams['projectId']) => { if (response.status === 200) return response.body } +export const syncRepository = async (projectId: SyncRepositoryParams['projectId'], repositoryId: SyncRepositoryParams['repositoryId'], branchName: SyncRepositoryParams['branchName']) => { + const response = await apiClient.Repositories.syncRepository({ params: { projectId, repositoryId, branchName } }) + return response.body +} + export const updateRepo = async (projectId: RepositoryParams['projectId'], repositoryId: RepositoryParams['repositoryId'], data: UpdateRepositoryBody) => { if (!data.id) return const response = await apiClient.Repositories.updateRepository({ body: data, params: { projectId, repositoryId } }) diff --git a/apps/client/src/components/RepoForm.vue b/apps/client/src/components/RepoForm.vue index dd5a93193..30fbc8e53 100644 --- a/apps/client/src/components/RepoForm.vue +++ b/apps/client/src/components/RepoForm.vue @@ -59,11 +59,11 @@ const cancel = () => { data-testid="repo-form" class="relative" > -

- {{ localRepo.id ? 'Modifier le dépôt' : 'Ajouter un dépôt au projet' }} -

+ {{ localRepo.id ? `Modifier le dépôt ${localRepo.internalRepoName}` : 'Ajouter un dépôt au projet' }} + { const projectStore = useProjectStore() + const syncRepository = async (repoId: string, branchName: string) => { + if (!projectStore.selectedProject) throw new Error(projectMissing) + await api.syncRepository(projectStore.selectedProject.id, repoId, branchName) + } + const addRepoToProject = async (newRepo: CreateRepositoryBody) => { if (!projectStore.selectedProject) throw new Error(projectMissing) await api.addRepo(projectStore.selectedProject.id, newRepo) @@ -29,5 +34,6 @@ export const useProjectRepositoryStore = defineStore('project-repository', () => addRepoToProject, updateRepo, deleteRepo, + syncRepository, } }) diff --git a/apps/client/src/views/projects/DsoRepos.vue b/apps/client/src/views/projects/DsoRepos.vue index f36707e1e..32e8bf048 100644 --- a/apps/client/src/views/projects/DsoRepos.vue +++ b/apps/client/src/views/projects/DsoRepos.vue @@ -23,6 +23,10 @@ const isOwner = computed(() => project.value?.roles?.some(role => role.userId == const repos = ref([]) const selectedRepo = ref() const isNewRepoForm = ref(false) +const branchName = ref('main') + +const repoFormId = 'repoFormId' +const syncFormId = 'syncFormId' const setReposTiles = (project: Project) => { // @ts-ignore @@ -75,6 +79,15 @@ const deleteRepo = async (repoId: Repo['id']) => { snackbarStore.isWaitingForResponse = false } +const syncRepository = async () => { + if (!selectedRepo.value) return + if (!branchName.value) branchName.value = 'main' + snackbarStore.isWaitingForResponse = true + projectRepositoryStore.syncRepository(selectedRepo.value.id, branchName.value) + snackbarStore.isWaitingForResponse = false + snackbarStore.setMessage(`Dépôt ${selectedRepo.value.internalRepoName} synchronisé`, 'success') +} + onMounted(() => { if (!project.value) return setReposTiles(project.value) @@ -149,15 +162,57 @@ watch(project, () => { @click="setSelectedRepo(repo.data)" /> - + > + +
+

+ Synchroniser le dépôt {{ selectedRepo?.internalRepoName }} +

+ + +
+ +
{ + try { + await getProjectAndcheckRole(userId, projectId) + + const { results } = await hook.misc.syncRepository(repositoryId, { branchName }) + + await addLogs('Sync Repository', results, userId, requestId) + if (results.failed) { + throw new UnprocessableContentError('Echec des opérations', undefined) + } + } catch (error) { + if (error instanceof DsoError) throw error + throw new Error('Echec de la synchronisation du dépôt') + } +} + export const checkUpsertRepository = async ( userId: User['id'], projectId: Project['id'], @@ -85,6 +107,7 @@ export const createRepository = async ( return repo } catch (error) { + if (error instanceof DsoError) throw error throw new Error('Echec de la création du dépôt') } } @@ -118,6 +141,7 @@ export const updateRepository = async ( return repo } catch (error) { + if (error instanceof DsoError) throw error throw new Error('Echec de la mise à jour du dépôt') } } @@ -139,6 +163,7 @@ export const deleteRepository = async ( throw new UnprocessableContentError('Echec des opérations', undefined) } } catch (error) { + if (error instanceof DsoError) throw error throw new Error('Echec de la mise à jour du dépôt') } } diff --git a/apps/server/src/resources/repository/controllers.spec.ts b/apps/server/src/resources/repository/controllers.spec.ts index fafd9fa34..34436075e 100644 --- a/apps/server/src/resources/repository/controllers.spec.ts +++ b/apps/server/src/resources/repository/controllers.spec.ts @@ -60,6 +60,38 @@ describe('Repository routes', () => { }) }) + describe('syncRepositoryController', () => { + it('Should sync a repository', async () => { + const projectInfos = createRandomDbSetup({}).project + projectInfos.roles = [...projectInfos.roles, getRandomRole(getRequestor().id, projectInfos.id, 'owner')] + const repoToSync = projectInfos.repositories[0] + const branchName = 'main' + + prisma.project.findUnique.mockResolvedValue(projectInfos) + + const response = await app.inject() + .get(`/api/v1/projects/${projectInfos.id}/repositories/${repoToSync.id}/sync/${branchName}`) + .end() + + expect(response.statusCode).toEqual(204) + }) + + it('Should not sync a repository if not project member', async () => { + const projectInfos = createRandomDbSetup({}).project + const repoToSync = projectInfos.repositories[0] + const branchName = 'main' + + prisma.project.findUnique.mockResolvedValue(projectInfos) + + const response = await app.inject() + .get(`/api/v1/projects/${projectInfos.id}/repositories/${repoToSync.id}/sync/${branchName}`) + .end() + + expect(response.statusCode).toEqual(403) + expect(JSON.parse(response.body).error).toEqual('Vous n’avez pas les permissions suffisantes dans le projet') + }) + }) + // POST describe('createRepositoryController', () => { it('Should create a repository', async () => { diff --git a/apps/server/src/resources/repository/queries.ts b/apps/server/src/resources/repository/queries.ts index 84a575de1..60e722946 100644 --- a/apps/server/src/resources/repository/queries.ts +++ b/apps/server/src/resources/repository/queries.ts @@ -26,6 +26,19 @@ export const initializeRepository = async ({ projectId, internalRepoName, extern }) } +export const getHookRepository = (id: Repository['id']) => prisma.repository.findUniqueOrThrow({ + where: { + id, + }, + include: { + project: { + include: { + organization: true, + }, + }, + }, +}) + // UPDATE export const updateRepository = async (id: Repository['id'], infos: Partial) => { return prisma.repository.update({ where: { id }, data: { ...infos } }) diff --git a/apps/server/src/resources/repository/router.ts b/apps/server/src/resources/repository/router.ts index 0871188f8..e3991d895 100644 --- a/apps/server/src/resources/repository/router.ts +++ b/apps/server/src/resources/repository/router.ts @@ -1,16 +1,17 @@ -import { filterObjectByKeys } from '@/utils/queries-tools.js' +import { repositoryContract } from '@cpn-console/shared' +import { serverInstance } from '@/app.js' +import { BadRequestError } from '@/utils/errors.js' import { addReqLogs } from '@/utils/logger.js' +import { filterObjectByKeys } from '@/utils/queries-tools.js' import { + checkUpsertRepository, createRepository, deleteRepository, getProjectRepositories, getRepositoryById, + syncRepository, updateRepository, - checkUpsertRepository, } from './business.js' -import { BadRequestError } from '@/utils/errors.js' -import { repositoryContract } from '@cpn-console/shared' -import { serverInstance } from '@/app.js' export const repositoryRouter = () => serverInstance.router(repositoryContract, { @@ -57,6 +58,19 @@ export const repositoryRouter = () => serverInstance.router(repositoryContract, } }, + // Synchroniser un repository + syncRepository: async ({ request: req, params }) => { + const userId = req.session.user.id + const { projectId, repositoryId, branchName } = params + + await syncRepository(projectId, repositoryId, userId, branchName, req.id) + + return { + body: null, + status: 204, + } + }, + // Créer un repository createRepository: async ({ request: req, params, body: data }) => { const userId = req.session.user.id diff --git a/apps/server/src/utils/hook-wrapper.ts b/apps/server/src/utils/hook-wrapper.ts index b736502ed..7bba1d7d5 100644 --- a/apps/server/src/utils/hook-wrapper.ts +++ b/apps/server/src/utils/hook-wrapper.ts @@ -2,7 +2,7 @@ import type { Cluster, Project } from '@prisma/client' import type { ClusterObject, KubeCluster, KubeUser, PluginResult, Project as ProjectPayload, RepoCreds, Repository } from '@cpn-console/hooks' import { hooks } from '@cpn-console/hooks' import { AsyncReturnType } from '@cpn-console/shared' -import { archiveProject, getClusterByIdOrThrow, getHookProjectInfos, getHookPublicClusters, updateProjectCreated, updateProjectFailed, updateProjectServices } from '@/resources/queries-index.js' +import { archiveProject, getClusterByIdOrThrow, getHookProjectInfos, getHookPublicClusters, getHookRepository, updateProjectCreated, updateProjectFailed, updateProjectServices } from '@/resources/queries-index.js' import { genericProxy } from './proxy.js' type ReposCreds = Record @@ -74,9 +74,17 @@ const cluster = { } const misc = { - fetchOrganizations: () => hooks.fetchOrganizations.execute({}), - retrieveUserByEmail: (email: string) => hooks.retrieveUserByEmail.execute({ email }), - checkServices: () => hooks.checkServices.execute({}), + fetchOrganizations: async () => hooks.fetchOrganizations.execute({}), + retrieveUserByEmail: async (email: string) => hooks.retrieveUserByEmail.execute({ email }), + checkServices: async () => hooks.checkServices.execute({}), + syncRepository: async (repoId: string, { branchName }: {branchName: string}) => { + const { project, ...repoInfos } = await getHookRepository(repoId) + const payload = { + repo: { ...repoInfos, branchName }, + ...project, + } + return hooks.syncRepository.execute(payload) + }, } export const hook = { diff --git a/apps/server/src/utils/mocks.ts b/apps/server/src/utils/mocks.ts index 538cbb16d..c54dac418 100644 --- a/apps/server/src/utils/mocks.ts +++ b/apps/server/src/utils/mocks.ts @@ -232,6 +232,7 @@ const misc = { fetchOrganizations: async () => resultsFetch, retrieveUserByEmail: async (_email: string) => resultsBase, checkServices: async () => resultsBase, + syncRepository: async () => resultsBase, } const project = { @@ -259,7 +260,7 @@ const cluster = { export const mockHookWrapper = () => ({ hook: { - misc: genericProxy(misc, { checkServices: [], fetchOrganizations: [], retrieveUserByEmail: [] }), + misc: genericProxy(misc, { checkServices: [], fetchOrganizations: [], retrieveUserByEmail: [], syncRepository: [] }), project: genericProxy(project, { delete: ['upsert'], upsert: ['delete'], getSecrets: ['delete'] }), cluster: genericProxy(cluster, { delete: ['upsert'], upsert: ['delete'] }), }, diff --git a/packages/hooks/src/hooks/hook-misc.ts b/packages/hooks/src/hooks/hook-misc.ts index f9ab72187..70f2029a1 100644 --- a/packages/hooks/src/hooks/hook-misc.ts +++ b/packages/hooks/src/hooks/hook-misc.ts @@ -1,4 +1,4 @@ -import { Project } from './hook-project.js' +import { Project, Repository } from './hook-project.js' import { Hook, createHook } from './hook.js' import { UserObject } from './index.js' @@ -14,3 +14,6 @@ export const retrieveUserByEmail: Hook = createHook() export type ProjectLite = Pick export const getProjectSecrets: Hook = createHook() + +export type UniqueRepo = ProjectLite & { repo: Omit & { branchName: string } } +export const syncRepository: Hook = createHook() diff --git a/packages/shared/src/contracts/repository.ts b/packages/shared/src/contracts/repository.ts index d9bde6769..3af3ff540 100644 --- a/packages/shared/src/contracts/repository.ts +++ b/packages/shared/src/contracts/repository.ts @@ -6,6 +6,7 @@ import { GetRepoByIdSchema, UpdateRepoSchema, DeleteRepoSchema, + SyncRepoSchema, } from '../schemas/index.js' export const repositoryContract = contractInstance.router({ @@ -38,6 +39,15 @@ export const repositoryContract = contractInstance.router({ responses: GetRepoByIdSchema.responses, }, + syncRepository: { + method: 'GET', + path: `${apiPrefix}/projects/:projectId/repositories/:repositoryId/sync/:branchName`, + pathParams: SyncRepoSchema.params, + summary: 'application/json', + description: 'Trigger a gitlab synchronization for a repository', + responses: SyncRepoSchema.responses, + }, + updateRepository: { method: 'PUT', path: `${apiPrefix}/projects/:projectId/repositories/:repositoryId`, @@ -63,4 +73,6 @@ export type CreateRepositoryBody = ClientInferRequest['body'] +export type SyncRepositoryParams = ClientInferRequest['params'] + export type RepositoryParams = ClientInferRequest['params'] diff --git a/packages/shared/src/schemas/repository.ts b/packages/shared/src/schemas/repository.ts index 378970052..f60fa35b2 100644 --- a/packages/shared/src/schemas/repository.ts +++ b/packages/shared/src/schemas/repository.ts @@ -93,6 +93,23 @@ export const UpdateRepoSchema = { }, } +export const SyncRepoSchema = { + params: z.object({ + projectId: z.string() + .uuid(), + repositoryId: z.string() + .uuid(), + branchName: z.string(), + }), + responses: { + 204: null, + 400: ErrorSchema, + 401: ErrorSchema, + 403: ErrorSchema, + 500: ErrorSchema, + }, +} + export const DeleteRepoSchema = { params: z.object({ projectId: z.string() diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 130d3b49e..8d2e0bd8b 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -15,6 +15,7 @@ "src/**/*.ts" ], "exclude": [ - "coverage" + "coverage", + "**/*.spec.ts", ] } \ No newline at end of file diff --git a/plugins/gitlab/src/class.ts b/plugins/gitlab/src/class.ts index c9d2f22cf..fb49f54f9 100644 --- a/plugins/gitlab/src/class.ts +++ b/plugins/gitlab/src/class.ts @@ -1,8 +1,8 @@ -import { PluginApi, Project, RepoCreds } from '@cpn-console/hooks' +import { PluginApi, type Project, type RepoCreds, type UniqueRepo } from '@cpn-console/hooks' import { getApi, getConfig, infraAppsRepoName, internalMirrorRepoName } from './utils.js' import { AccessTokenScopes, GroupSchema, GroupStatisticsSchema, MemberSchema, ProjectVariableSchema, VariableSchema } from '@gitbeaker/rest' import { getOrganizationId } from './group.js' -import { AccessLevel, Gitlab } from '@gitbeaker/core' +import { AccessLevel, CondensedProjectSchema, Gitlab } from '@gitbeaker/core' import { VaultProjectApi } from '@cpn-console/vault-plugin/types/class.js' type setVariableResult = 'created' | 'updated' | 'already up-to-date' @@ -13,14 +13,19 @@ type GitlabMirrorSecret = { MIRROR_TOKEN: string, } +type RepoSelect = { + mirror?: CondensedProjectSchema + target?: CondensedProjectSchema +} + export class GitlabProjectApi extends PluginApi { private api: Gitlab - private project: Project + private project: Project | UniqueRepo private gitlabGroup: GroupSchema & { statistics: GroupStatisticsSchema } | undefined private specialRepositories: string[] = [infraAppsRepoName, internalMirrorRepoName] // private organizationGroup: GroupSchema & { statistics: GroupStatisticsSchema } | undefined - constructor (project: Project) { + constructor (project: Project | UniqueRepo) { super() this.project = project this.api = getApi() @@ -39,7 +44,6 @@ export class GitlabProjectApi extends PluginApi { projectCreationLevel: 'maintainer', subgroupCreationLevel: 'owner', defaultBranchProtection: 0, - description: this.project.description ?? '', }) } @@ -200,9 +204,9 @@ export class GitlabProjectApi extends PluginApi { } else { if ( currentVariable.masked !== toSetVariable.masked || - currentVariable.value !== toSetVariable.value || - currentVariable.protected !== toSetVariable.protected || - currentVariable.variable_type !== toSetVariable.variable_type + currentVariable.value !== toSetVariable.value || + currentVariable.protected !== toSetVariable.protected || + currentVariable.variable_type !== toSetVariable.variable_type ) { await this.api.GroupVariables.edit( group.id, @@ -231,9 +235,9 @@ export class GitlabProjectApi extends PluginApi { if (currentVariable) { if ( currentVariable.masked !== toSetVariable.masked || - currentVariable.value !== toSetVariable.value || - currentVariable.protected !== toSetVariable.protected || - currentVariable.variable_type !== toSetVariable.variable_type + currentVariable.value !== toSetVariable.value || + currentVariable.protected !== toSetVariable.protected || + currentVariable.variable_type !== toSetVariable.variable_type ) { await this.api.ProjectVariables.edit( repository.id, @@ -263,4 +267,33 @@ export class GitlabProjectApi extends PluginApi { return 'created' } } + + // Mirror + public async triggerMirror (targetRepo: string, branchName: string) { + if ((await this.getSpecialRepositories()).includes(targetRepo)) throw new Error('User requested for invalid mirroring') + const repos = await this.listRepositories() + const { mirror, target }: RepoSelect = repos.reduce((acc, repository) => { + if (repository.name === 'mirror') { + acc.mirror = repository + } + if (repository.name === targetRepo) { + acc.target = repository + } + return acc + }, {} as RepoSelect) + if (!mirror) throw new Error('Unable to find mirror repository') + if (!target) throw new Error('Unable to find target repository') + return this.api.Pipelines.create(mirror.id, 'main', { + variables: [ + { + key: 'GIT_BRANCH_DEPLOY', + value: branchName, + }, + { + key: 'PROJECT_NAME', + value: targetRepo, + }, + ], + }) + } } diff --git a/plugins/gitlab/src/functions.ts b/plugins/gitlab/src/functions.ts index 1fe2c41c6..25fc3ef77 100644 --- a/plugins/gitlab/src/functions.ts +++ b/plugins/gitlab/src/functions.ts @@ -1,4 +1,4 @@ -import { type StepCall, type Project, type ProjectLite, parseError } from '@cpn-console/hooks' +import { type StepCall, type Project, type ProjectLite, parseError, type UniqueRepo } from '@cpn-console/hooks' import { deleteGroup } from './group.js' import { createUsername, getUser } from './user.js' import { ensureMembers } from './members.js' @@ -131,3 +131,25 @@ export const deleteDsoProject: StepCall = async (payload) => { } } } + +export const syncRepository: StepCall = async (payload) => { + const targetRepo = payload.args.repo + const gitlabApi = payload.apis.gitlab + try { + await gitlabApi.triggerMirror(targetRepo.internalRepoName, targetRepo.branchName) + return { + status: { + result: 'OK', + message: 'Ci launched', + }, + } + } catch (error) { + return { + error: parseError(error), + status: { + result: 'KO', + message: 'Failed to trigger sync', + }, + } + } +} diff --git a/plugins/gitlab/src/index.ts b/plugins/gitlab/src/index.ts index 1fd676d41..ecf1a4252 100644 --- a/plugins/gitlab/src/index.ts +++ b/plugins/gitlab/src/index.ts @@ -1,16 +1,17 @@ -import type { Plugin, Project, DefaultArgs } from '@cpn-console/hooks' +import type { Plugin, Project, DefaultArgs, UniqueRepo } from '@cpn-console/hooks' import { checkApi, getDsoProjectSecrets, deleteDsoProject, upsertDsoProject, + syncRepository, } from './functions.js' import { getGroupRootId } from './utils.js' import infos from './infos.js' import monitor from './monitor.js' import { GitlabProjectApi } from './class.js' -const onlyApi = { api: (project: Project) => new GitlabProjectApi(project) } +const onlyApi = { api: (project: Project | UniqueRepo) => new GitlabProjectApi(project) } const start = () => { getGroupRootId() @@ -31,6 +32,12 @@ export const plugin: Plugin = { }, }, getProjectSecrets: { steps: { main: getDsoProjectSecrets } }, + syncRepository: { + ...onlyApi, + steps: { + main: syncRepository, + }, + }, }, monitor, start, @@ -38,7 +45,7 @@ export const plugin: Plugin = { declare module '@cpn-console/hooks' { interface HookPayloadApis { - gitlab: Args extends Project + gitlab: Args extends Project | UniqueRepo ? GitlabProjectApi : undefined }