platform/services/github/pod-github/src/sync/repository.ts
Andrey Sobolev 65d45d7e82
Few github high cpu load fixes ()
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
2024-10-10 21:13:04 +07:00

395 lines
13 KiB
TypeScript

//
// Copyright © 2023 Hardcore Engineering Inc.
//
//
import core, { Doc, DocData, DocumentUpdate, MeasureContext, TxOperations, generateId } from '@hcengineering/core'
import github, { DocSyncInfo, GithubIntegrationRepository, GithubProject } from '@hcengineering/github'
import { Endpoints } from '@octokit/types'
import {
Repository,
RepositoryEvent,
type InstallationCreatedEvent,
type InstallationUnsuspendEvent
} from '@octokit/webhooks-types'
import { App } from 'octokit'
import { DocSyncManager, ExternalSyncField, IntegrationContainer, IntegrationManager } from '../types'
import { collectUpdate } from './utils'
const syncReposKey = 'repo_sync'
export class RepositorySyncMapper implements DocSyncManager {
constructor (
private readonly ctx: MeasureContext,
private readonly client: TxOperations,
private readonly app: App
) {}
externalDerivedSync = false
provider!: IntegrationManager
// Initialize the mapper.
async init (provider: IntegrationManager): Promise<void> {
this.provider = provider
}
// Perform synchronization of document with external source.
async sync (existing: Doc | undefined, info: DocSyncInfo): Promise<DocumentUpdate<DocSyncInfo> | undefined> {
return {}
}
async reloadRepositories (
integration: IntegrationContainer,
repositories?: InstallationCreatedEvent['repositories'] | InstallationUnsuspendEvent['repositories']
): Promise<void> {
integration.synchronized.delete(syncReposKey)
if (repositories !== undefined) {
// We have a list of repositories, so we could create them if they are missing.
// Need to find all repositories, not only active, so passed repositories are not work.
const allRepositories = (
await this.provider.liveQuery.queryFind(github.class.GithubIntegrationRepository, {})
).filter((it) => it.attachedTo === integration.integration._id)
const allRepos: GithubIntegrationRepository[] = [...allRepositories]
for (const repository of repositories) {
const integrationRepo: GithubIntegrationRepository | undefined = allRepos.find(
(it) => it.repositoryId === repository.id
)
if (integrationRepo === undefined) {
// No integration repository found, we need to push one.
await this.client.addCollection(
github.class.GithubIntegrationRepository,
integration.integration.space,
integration.integration._id,
integration.integration._class,
'repositories',
{
nodeId: repository.node_id,
name: repository.name,
url: integration.installationName + '/' + repository.name,
repositoryId: repository.id,
enabled: true,
deleted: false,
archived: false,
fork: false,
forks: 0,
hasDiscussions: false,
hasDownloads: false,
hasIssues: false,
hasPages: false,
hasProjects: false,
hasWiki: false,
openIssues: 0,
private: repository.private,
size: 0,
stargazers: 0,
watchers: 0,
visibility: repository.private ? 'private' : 'public'
},
undefined, // id
Date.now(),
integration.integration.createdBy
)
this.ctx.info('Creating repository info document...', {
url: repository.full_name,
workspace: this.provider.getWorkspaceId().name
})
}
}
}
}
async handleEvent<T>(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise<void> {
const event = evt as RepositoryEvent
const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System
switch (event.action) {
case 'created': {
await this.client.addCollection(
github.class.GithubIntegrationRepository,
integration.integration.space,
integration.integration._id,
integration.integration._class,
'repositories',
{
...this.getRData(event.repository),
name: event.repository.name,
repositoryId: event.repository.id,
enabled: true
},
generateId(),
Date.now(),
account
)
this.ctx.info('Creating repository info document...', {
url: event.repository.url,
workspace: this.provider.getWorkspaceId().name
})
break
}
case 'renamed': {
const githubRepo = await this.client.findOne(github.class.GithubIntegrationRepository, {
repositoryId: event.repository.id
})
if (githubRepo !== undefined) {
await this.client.update(
githubRepo,
{
name: event.repository.name
},
false,
Date.now(),
account
)
githubRepo.name = event.repository.name
const allProjects = await this.client.findAll(github.mixin.GithubProject, { repositories: githubRepo?._id })
for (const prj of allProjects) {
// We need to force sync
await this.handleRepoRename(integration, prj, githubRepo)
}
}
break
}
case 'deleted':
case 'transferred': {
// TODO: Add remove of component
const githubRepo = await this.client.findOne(github.class.GithubIntegrationRepository, {
integration: integration.integration._id,
name: event.repository.name
})
if (githubRepo !== undefined) {
await this.client.update(
githubRepo,
{
enabled: true,
deleted: true
},
false,
Date.now(),
account
)
}
}
}
}
async handleDelete (
existing: Doc | undefined,
info: DocSyncInfo,
derivedClient: TxOperations,
deleteExisting: boolean
): Promise<boolean> {
return false
}
getRData (
repository: Repository | Endpoints['GET /installation/repositories']['response']['data']['repositories'][0]
): Omit<DocData<GithubIntegrationRepository>, 'name' | 'repositoryId' | 'deleted' | 'githubProjects' | 'enabled'> {
return {
nodeId: repository.node_id,
url: repository.url,
htmlURL: repository.html_url,
owner: {
id: repository.owner.node_id,
login: repository.owner.login,
avatarUrl: repository.owner.avatar_url,
email: repository.owner.email ?? undefined,
name: repository.owner.name ?? undefined
},
description: repository.description ?? undefined,
fork: repository.fork,
forks: repository.forks,
private: repository.private,
stargazers: repository.stargazers_count,
hasIssues: repository.has_issues,
hasProjects: repository.has_projects,
hasDownloads: repository.has_downloads,
hasPages: repository.has_pages,
hasWiki: repository.has_wiki,
hasDiscussions: repository.has_discussions ?? false,
openIssues: repository.open_issues,
watchers: repository.watchers_count,
archived: repository.archived,
size: repository.size,
language: repository.language ?? undefined,
resourcePath: repository.full_name,
visibility: repository.visibility,
updatedAt: new Date(repository.updated_at ?? repository.created_at ?? Date.now()).getTime()
}
}
async externalSync (
integration: IntegrationContainer,
derivedClient: TxOperations,
kind: ExternalSyncField,
syncDocs: DocSyncInfo[],
repo: GithubIntegrationRepository,
prj: GithubProject
): Promise<void> {}
repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void {}
async externalFullSync (
integration: IntegrationContainer,
derivedClient: TxOperations,
projects: GithubProject[],
repositories: GithubIntegrationRepository[]
): Promise<void> {
const inst = integration.octokit
if (inst === undefined || integration.octokit === undefined) {
this.ctx.info('no installation found', { workspace: this.provider.getWorkspaceId().name })
return
}
if (integration.synchronized.has(syncReposKey)) {
return
}
this.ctx.info('Checking github installation repositories...', {
installationId: integration.installationId,
workspace: this.provider.getWorkspaceId().name
})
const iterable = this.app.eachRepository.iterator({ installationId: integration.installationId })
// Need to find all repositories, not only active, so passed repositories are not work.
const allRepositories = (
await this.provider.liveQuery.queryFind(github.class.GithubIntegrationRepository, {})
).filter((it) => it.attachedTo === integration.integration._id)
let allRepos: GithubIntegrationRepository[] = [...allRepositories]
const githubRepos:
| Repository
| Endpoints['GET /installation/repositories']['response']['data']['repositories'][0][] = []
for await (const { repository } of iterable) {
githubRepos.push(repository)
}
for (const repository of githubRepos) {
const integrationRepo: GithubIntegrationRepository | undefined = allRepos.find(
(it) => it.repositoryId === repository.id
)
const rdata = this.getRData(repository)
if (integrationRepo === undefined) {
// No integration repository found, we need to push one.
await this.client.addCollection(
github.class.GithubIntegrationRepository,
integration.integration.space,
integration.integration._id,
integration.integration._class,
'repositories',
{
...rdata,
name: repository.name,
repositoryId: repository.id,
enabled: true,
deleted: false
},
undefined, // id
Date.now(),
integration.integration.createdBy
)
this.ctx.info('Creating repository info document...', {
url: repository.url,
workspace: this.provider.getWorkspaceId().name
})
} else {
allRepos = allRepos.filter((it) => it._id !== integrationRepo._id)
const diff = collectUpdate(
integrationRepo,
{
name: repository.name,
...rdata
},
['name', ...Object.keys(rdata)]
)
if (Object.keys(diff).length > 0) {
this.ctx.info('processing repository diff update...', {
repository: repository.name,
...diff,
workspace: this.provider.getWorkspaceId().name
})
await this.client.diffUpdate(
integrationRepo,
{
name: repository.name,
...rdata
},
new Date().getTime(),
integration.integration.createdBy
)
}
}
}
// Ok we have repos removed from integration, we need to delete them.
for (const repo of allRepos) {
// Mark as archived
await this.client.update(repo, { archived: true })
}
// We need to delete and disconnect missing repositories.
integration.synchronized.add(syncReposKey)
}
// Perform a synchronization of a single repository.
async handleRepoRename (
integration: IntegrationContainer,
prj: GithubProject,
repo: GithubIntegrationRepository
): Promise<void> {
// We need to update urls for all sync documents belong to this repository.
const derivedClient = new TxOperations(this.client, core.account.System, true)
const processingId = generateId()
// Wait previous sync to finish
await integration.syncLock.get(prj._id)
/**
Variants:
"https://api.github.com/repos/hcengineering/anticrm/issues/comments/1679316918"
"https://github.com/hcengineering/uberflow/pull/195"
* */
this.ctx.info('handle repository rename', { repo, workspace: this.provider.getWorkspaceId().name })
const update = async (): Promise<void> => {
while (true) {
const docs = await this.client.findAll(
github.class.DocSyncInfo,
{ _class: github.class.DocSyncInfo, repository: repo._id, processingId: { $ne: processingId } },
{ limit: 1000 }
)
const ops = derivedClient.apply()
if (docs.length === 0) {
break
}
for (const d of docs) {
const ul = d.url.split('/')
if (ul[2] === 'api.github.com') {
ul[5] = repo.name
} else {
ul[4] = repo.name
}
// We need to mark sync is required, to perform github
await ops.diffUpdate(d, { url: ul.join('/'), processingId, needSync: '', externalVersion: '' })
}
await ops.commit()
this.provider.sync()
}
}
const p = update()
integration.syncLock.set(prj._id, p)
await p
integration.syncLock.delete(prj._id)
}
}