Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions apps/client/src/components/ProjectResources.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { CleanedCluster, Cluster, CreateEnvironment, Environment, EnvironmentWithDeploymentsCount, Repo, Stage, UpdateEnvironment, Zone } from '@cpn-console/shared'
import type { CleanedCluster, Cluster, CreateEnvironment, Environment, EnvironmentWithDeploymentsCount, Repo, Stage, SyncRepositoryBodyV2, UpdateEnvironment, Zone } from '@cpn-console/shared'
import type { Project } from '@/utils/project-utils.js'
import type { RepoFormResult } from '@/utils/repository-utils.js'
import { logger } from '@cpn-console/logger/browser'
Expand Down Expand Up @@ -65,8 +65,8 @@ const canManageEnvs = computed(() => !props.project.locked && props.asProfile ==
const canManageRepos = computed(() => !props.project.locked && props.asProfile === 'user' && ProjectAuthorized.ManageRepositories({ projectPermissions: props.project.myPerms }))
const canListDeploy = computed(() => props.asProfile === 'admin' || ProjectAuthorized.ListDeployments({ projectPermissions: props.project.myPerms }))

watch(selectedRepo, async () => {
branchName.value = defaultBranchName
watch(selectedRepo, () => {
branchName.value = selectedRepo.value?.branchName ?? defaultBranchName
})

async function putEnvironment(environment: UpdateEnvironment, envId: Environment['id']) {
Expand Down Expand Up @@ -123,12 +123,17 @@ async function syncRepository() {
const repo = selectedRepo.value
if (!repo) return
if (!isAllSyncing.value && !branchName.value) branchName.value = defaultBranchName
await props.project.Repositories.sync(
repo.id,
isAllSyncing.value
? { syncAllBranches: true }
: { syncAllBranches: false, branchName: branchName.value },
)
const syncRequest: SyncRepositoryBodyV2 = isAllSyncing.value
? { syncAllBranches: true }
: { syncAllBranches: false, branchName: branchName.value }
await props.project.Repositories.sync(repo.id, syncRequest)
if (!syncRequest.syncAllBranches) {
repositories.value = repositories.value.map(repository =>
repository.id === repo.id
? { ...repository, branchName: syncRequest.branchName }
: repository,
Comment thread
StephaneTrebel marked this conversation as resolved.
)
}
snackbarStore.setMessage(`Travail de synchronisation lancé pour le dépôt ${repo.internalRepoName}`)
}

Expand Down
6 changes: 6 additions & 0 deletions apps/client/src/utils/repository-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ describe('toUpdateRepositoryBody', () => {
expect(body).not.toHaveProperty('isStandalone')
})

it('should omit the branch persisted by a synchronization', () => {
const body = toUpdateRepositoryBody(makeFormResult({ ...persisted, branchName: 'develop' }))

expect(body).not.toHaveProperty('branchName')
})

it('should omit an unchanged token, signalled by the fakeToken placeholder', () => {
const body = toUpdateRepositoryBody(makeFormResult({ ...persisted, isPrivate: true, externalUserName: 'this-is-tobi', externalToken: fakeToken }))

Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/utils/repository-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function toCreateRepositoryBody(repo: RepoFormResult): CreateRepositoryBo
}

export function toUpdateRepositoryBody(repo: RepoFormResult): UpdateRepositoryBodyV2 {
const { externalToken, id: _id, projectId: _projectId, createdAt: _createdAt, updatedAt: _updatedAt, internalRepoName: _internalRepoName, isStandalone: _isStandalone, ...repositoryFields } = repo
const { externalToken, id: _id, projectId: _projectId, createdAt: _createdAt, updatedAt: _updatedAt, internalRepoName: _internalRepoName, branchName: _branchName, isStandalone: _isStandalone, ...repositoryFields } = repo

return {
...repositoryFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function makeRepository(overrides: Partial<Repository> = {}): Repository
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: faker.string.alphanumeric(8).toLowerCase(),
branchName: 'main',
externalRepoUrl: '',
externalUserName: '',
isInfra: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ describe('repositoryDatastoreService', () => {
})
})

describe('updateBranchName', () => {
it('updates the persisted synchronization branch', async () => {
const repository = makeInternalRepository({ projectId })
prisma.repository.update.mockResolvedValue(repository)

await service.updateBranchName(repository.id, 'develop')

expect(prisma.repository.update).toHaveBeenCalledWith({
where: { id: repository.id },
data: { branchName: 'develop' },
})
})
})

describe('deleteRepository', () => {
it('returns the deleted repository', async () => {
const repository = makeInternalRepository()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export class RepositoryDatastoreService {
})
}

updateBranchName(repositoryId: string, branchName: string): Promise<Repository> {
return this.prisma.repository.update({
where: { id: repositoryId },
data: { branchName },
})
}

deleteRepository(repositoryId: string): Promise<Repository> {
return this.prisma.repository.delete({
where: { id: repositoryId },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function makeRepository(overrides: Partial<Repository> = {}): Repository
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: faker.string.alphanumeric(8).toLowerCase(),
branchName: 'main',
externalRepoUrl: '',
externalUserName: '',
isInfra: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,38 +321,54 @@ describe('repositoryService', () => {
},
{ action: 'Sync Repository', userId, requestId },
)
expect(datastore.updateBranchName).not.toHaveBeenCalled()
})

it('forwards the requested branch when not syncing every branch', async () => {
it('persists the requested branch before triggering its mirror', async () => {
const repository = makeRepository({ id: repositoryId, projectId })
const branchName = faker.git.branch()
const calls: string[] = []
datastore.getRepositoryById.mockResolvedValue(repository)
appEvents.emitRepositoryEvent.mockResolvedValue({})
datastore.updateBranchName.mockImplementation(async () => {
calls.push('persist')
return repository
})
appEvents.emitRepositoryEvent.mockImplementation(async () => {
calls.push('mirror')
return {}
})

await service.syncRepository(projectId, projectSlug, repositoryId, { syncAllBranches: false, branchName }, userId, requestId)

expect(datastore.updateBranchName).toHaveBeenCalledWith(repositoryId, branchName)
expect(calls).toEqual(['persist', 'mirror'])
expect(appEvents.emitRepositoryEvent).toHaveBeenCalledWith(
'repository.sync',
expect.objectContaining({ syncAllBranches: false, branchName }),
expect.objectContaining({ action: 'Sync Repository' }),
)
})

it('rejects with 422 when a plugin fails the synchronization', async () => {
datastore.getRepositoryById.mockResolvedValue(makeRepository({ id: repositoryId, projectId }))
it('keeps the requested branch when a plugin fails the synchronization', async () => {
const branchName = faker.git.branch()
const repository = makeRepository({ id: repositoryId, projectId })
datastore.getRepositoryById.mockResolvedValue(repository)
datastore.updateBranchName.mockResolvedValue(repository)
appEvents.emitRepositoryEvent.mockResolvedValue({
gitlab: { status: 'KO', message: 'Unable to find mirror repository', executionTime: 1, error: new Error('boom') },
})

await expect(service.syncRepository(projectId, projectSlug, repositoryId, syncRequest, userId, requestId))
await expect(service.syncRepository(projectId, projectSlug, repositoryId, { syncAllBranches: false, branchName }, userId, requestId))
.rejects.toThrow(UnprocessableEntityException)
expect(datastore.updateBranchName).toHaveBeenCalledWith(repositoryId, branchName)
})

it('rejects when the repository belongs to another project', async () => {
datastore.getRepositoryById.mockResolvedValue(makeRepository({ id: repositoryId, projectId: faker.string.uuid() }))

await expect(service.syncRepository(projectId, projectSlug, repositoryId, syncRequest, userId, requestId))
.rejects.toThrow(NotFoundException)
expect(datastore.updateBranchName).not.toHaveBeenCalled()
expect(appEvents.emitRepositoryEvent).not.toHaveBeenCalled()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class RepositoryService {
async syncRepository(projectId: string, projectSlug: string, repositoryId: string, syncRequest: SyncRepository, userId: string, requestId: string): Promise<void> {
const repository = await this.getProjectRepositoryOrThrow(projectId, repositoryId)

if (!syncRequest.syncAllBranches) {
await this.repositoryDatastoreService.updateBranchName(repositoryId, syncRequest.branchName)
}
Comment thread
StephaneTrebel marked this conversation as resolved.

const results = await this.syncRepositoryMirror(
{
projectId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "Repository" ADD COLUMN "branchName" TEXT NOT NULL DEFAULT 'main';
1 change: 1 addition & 0 deletions apps/server-nestjs/src/prisma/schema/project.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ model Repository {
id String @id @default(uuid()) @db.Uuid
projectId String @db.Uuid
internalRepoName String
branchName String @default("main")
externalRepoUrl String @default("")
externalUserName String @default("")
isInfra Boolean @default(false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "Repository" ADD COLUMN "branchName" TEXT NOT NULL DEFAULT 'main';
1 change: 1 addition & 0 deletions apps/server/src/prisma/schema/project.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ model Repository {
id String @id @default(uuid()) @db.Uuid
projectId String @db.Uuid
internalRepoName String
branchName String @default("main")
externalRepoUrl String @default("")
externalUserName String @default("")
isInfra Boolean @default(false)
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/contracts/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const repositoryContract = contractInstance.router({
contentType: 'application/json',
summary: 'Create repo',
description: 'Create new repo.',
body: RepoSchema.omit({ id: true, createdAt: true, updatedAt: true }),
body: RepoSchema.omit({ id: true, createdAt: true, updatedAt: true, branchName: true }),
responses: {
201: RepoSchema,
400: ErrorSchema,
Expand Down Expand Up @@ -71,7 +71,7 @@ export const repositoryContract = contractInstance.router({
repositoryId: z.string()
.uuid(),
}),
body: RepoSchema.omit({ createdAt: true, updatedAt: true }).partial(),
body: RepoSchema.omit({ createdAt: true, updatedAt: true, branchName: true }).partial(),
responses: {
200: RepoSchema,
500: ErrorSchema,
Expand Down
21 changes: 21 additions & 0 deletions packages/shared/src/schemas/repository.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { faker } from '@faker-js/faker'
import { describe, expect, it } from 'vitest'
import { defaultBranchName } from '../utils/const.js'
import { RepoSchema } from './repository.js'

describe('repoSchema', () => {
it('defaults a repository response without a branch to main', () => {
const repository = RepoSchema.parse({
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: 'my-repo',
externalRepoUrl: '',
isPrivate: false,
isInfra: false,
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.past().toISOString(),
})

expect(repository.branchName).toBe(defaultBranchName)
})
})
4 changes: 3 additions & 1 deletion packages/shared/src/schemas/repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type Zod from 'zod'
import { z } from 'zod'
import { forbiddenRepoNames, invalidGitUrl, invalidInternalRepoName, missingCredentials } from '../utils/const.js'
import { defaultBranchName, forbiddenRepoNames, invalidGitUrl, invalidInternalRepoName, missingCredentials } from '../utils/const.js'
import { AtDatesToStringExtend } from './_utils.js'

export const RepoSchema = z.object({
Expand All @@ -26,6 +26,8 @@ export const RepoSchema = z.object({
.optional(),
projectId: z.string()
.uuid(),
branchName: z.string()
.default(defaultBranchName),
// Optional deployment settings for infra repositories
deployRevision: z.string().optional(),
deployPath: z.string().optional(),
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/src/utils/schemas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Log, ProjectV2 } from '../index.js'
import { faker } from '@faker-js/faker'
import { describe, expect, it } from 'vitest'
import { ZodError } from 'zod'
import { ClusterDetailsSchema, ClusterPrivacy, ClusterUsageSchema, descriptionMaxLength, EnvironmentSchema, instanciateSchema, LogSchema, parseZodError, ProjectSchemaV2, RepoSchema, StageSchema, UserSchema } from '../index.js'
import { ClusterDetailsSchema, ClusterPrivacy, ClusterUsageSchema, defaultBranchName, descriptionMaxLength, EnvironmentSchema, instanciateSchema, LogSchema, parseZodError, ProjectSchemaV2, RepoSchema, StageSchema, UserSchema } from '../index.js'

describe('schemas utils', () => {
it('should delete config in log', () => {
Expand Down Expand Up @@ -44,6 +44,7 @@ describe('schemas utils', () => {
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: 'candilib',
branchName: defaultBranchName,
externalRepoUrl: 'https://github.com/LAB-MI/candilibV2.git',
externalToken: 'eddddsqsq-_',
isPrivate: true,
Expand Down Expand Up @@ -159,6 +160,7 @@ describe('schemas utils', () => {
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: 'candilib',
branchName: defaultBranchName,
externalRepoUrl: 'https://github.com/LAB-MI/candilibV2.git',
externalUserName: 'clairenlet',
externalToken: 'myToken',
Expand Down Expand Up @@ -213,6 +215,7 @@ describe('schemas utils', () => {
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: '-candilib',
branchName: defaultBranchName,
externalRepoUrl: 'https://github.com/LAB-MI/candilibV2.git',
isPrivate: false,
isInfra: false,
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/src/utils/random-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function getRandomRepo(projectId = faker.string.uuid()): Repository {
id: faker.string.uuid(),
projectId,
internalRepoName: faker.lorem.word(),
branchName: 'main',
externalRepoUrl: getRandomGitUrl(),
isPrivate: faker.datatype.boolean(),
isInfra: faker.datatype.boolean(),
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface Repository {
id: string
projectId: string
internalRepoName: string
branchName: string
externalRepoUrl: string
isPrivate: boolean
isInfra: boolean
Expand Down
17 changes: 17 additions & 0 deletions playwright/e2e-tests/repos.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
invalidInternalRepoErrorMessage,
} from '../helpers/constants'
import { createProject, projectSlugTextRegexp } from '../helpers/project'
import { createRepository as createRepositoryThroughUi } from '../helpers/repository'

test.describe('Repositories', () => {
// @TODO: Rework this Cypress-inherited test (use of following-sibling is a
Expand Down Expand Up @@ -501,6 +502,11 @@ test.describe('Repositories', () => {
await expect(
page.getByTestId(`repoTr-${repo.internalRepoName}`),
).toBeVisible()
const secondRepositoryName = await createRepositoryThroughUi({
page,
repositoryName: 'repo04',
externalRepoUrlInput: 'https://github.com/externalUser04/repo04.git',
})

// Assert - Update repository
await page.getByTestId(`repoTr-repo03`).click()
Expand Down Expand Up @@ -537,6 +543,17 @@ test.describe('Repositories', () => {
'Travail de synchronisation lancé pour le dépôt repo03Fermer le message',
),
).toBeVisible()
await page.getByTestId('resource-modal').getByRole('button', { name: 'Fermer' }).click()
await page.getByTestId(`repoTr-${repo.internalRepoName}`).click()
await expect(page.getByTestId('branchNameInput')).toHaveValue('develop')
await page.getByTestId('resource-modal').getByRole('button', { name: 'Fermer' }).click()
await page.reload()
await expect(page.locator('h1')).toContainText(projectName)
await page.getByTestId(`repoTr-${repo.internalRepoName}`).click()
await expect(page.getByTestId('branchNameInput')).toHaveValue('develop')
await page.getByTestId('resource-modal').getByRole('button', { name: 'Fermer' }).click()
await page.getByTestId(`repoTr-${secondRepositoryName}`).click()
await expect(page.getByTestId('branchNameInput')).toHaveValue('main')
},
)

Expand Down
Loading