diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index 836491ca69..2732481d0f 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -82,11 +82,15 @@ export class Hierarchy { return m ?? doc._class } - hasMixin(doc: D, mixin: Ref>): boolean { + static hasMixin(doc: D, mixin: Ref>): boolean { const d = Hierarchy.toDoc(doc) return typeof (d as any)[mixin] === 'object' } + hasMixin(doc: D, mixin: Ref>): boolean { + return Hierarchy.hasMixin(doc, mixin) + } + classHierarchyMixin( _class: Ref>, mixin: Ref>, diff --git a/server/postgres/src/schemas.ts b/server/postgres/src/schemas.ts index bb45ce0724..e05669a5b6 100644 --- a/server/postgres/src/schemas.ts +++ b/server/postgres/src/schemas.ts @@ -239,6 +239,11 @@ const docSyncInfo: Schema = { notNull: false, index: false }, + parent: { + type: 'text', + notNull: false, + index: false + }, objectClass: { type: 'text', notNull: false, diff --git a/services/github/github-assets/lang/en.json b/services/github/github-assets/lang/en.json index 3ca4e656fc..5fbb082ef4 100644 --- a/services/github/github-assets/lang/en.json +++ b/services/github/github-assets/lang/en.json @@ -46,6 +46,7 @@ "Options": "Options", "Projects": "Projects ({count})", "GithubIssue": "GitHub Issue", + "GithubMilestone": "GitHub Milestone(Project)", "OnlyOrganizationError": "Only organization accounts are supported", "Mergeable": "Ready to Merge", "IssueRepositoryTarget": "into", diff --git a/services/github/github-assets/lang/pt.json b/services/github/github-assets/lang/pt.json index ce5646cb18..29058c8671 100644 --- a/services/github/github-assets/lang/pt.json +++ b/services/github/github-assets/lang/pt.json @@ -46,6 +46,7 @@ "Options": "Opções", "Projects": "Projetos ({count})", "GithubIssue": "Issue do Github", + "GithubMilestone": "GitHub Milestone(Project)", "OnlyOrganizationError": "Somente contas de organização são suportadas", "Mergeable": "Pronto para ser mergeado", "IssueRepositoryTarget": "em", diff --git a/services/github/github-assets/lang/ru.json b/services/github/github-assets/lang/ru.json index 4cf09f2d7c..b913aeba60 100644 --- a/services/github/github-assets/lang/ru.json +++ b/services/github/github-assets/lang/ru.json @@ -46,6 +46,7 @@ "Options": "Опции", "Projects": "Проекты ({count})", "GithubIssue": "Github тикет", + "GithubMilestone": "GitHub Майлстоун (Проект)", "OnlyOrganizationError": "Поддерживаются только Аккаунты организации", "Mergeable": "Готовность к влитию", "IssueRepositoryTarget": "Репозиторий", diff --git a/services/github/github-assets/lang/sp.json b/services/github/github-assets/lang/sp.json index 5b6e36484d..1848350fc3 100644 --- a/services/github/github-assets/lang/sp.json +++ b/services/github/github-assets/lang/sp.json @@ -46,6 +46,7 @@ "Options": "Opciones", "Projects": "Proyectos ({count})", "GithubIssue": "Problema de Github", + "GithubMilestone": "GitHub Milestone(Project)", "OnlyOrganizationError": "Solo se admiten cuentas de organizaciones", "Mergeable": "Listo para combinar", "IssueRepositoryTarget": "en", diff --git a/services/github/model-github/src/index.ts b/services/github/model-github/src/index.ts index 2ea37c205b..c849636246 100644 --- a/services/github/model-github/src/index.ts +++ b/services/github/model-github/src/index.ts @@ -434,18 +434,20 @@ export class TGithubComponent extends TComponent implements GithubComponent { } @Mixin(github.mixin.GithubMilestone, tracker.class.Milestone) -@UX(github.string.GithubIssue) +@UX(github.string.GithubMilestone) export class TGithubMilestone extends TMilestone implements GithubMilestone { - @Prop(TypeHyperlink(), getEmbeddedLabel('Github URL')) + @Prop(TypeHyperlink(), getEmbeddedLabel('Github Project URL')) @Index(IndexKind.FullText) @ReadOnly() url!: Hyperlink @Prop(TypeString(), getEmbeddedLabel('NodeID')) + @Hidden() @ReadOnly() projectNodeId!: string @Prop(TypeNumber(), getEmbeddedLabel('Number')) + @Hidden() @ReadOnly() projectNumber!: number diff --git a/services/github/model-github/src/plugin.ts b/services/github/model-github/src/plugin.ts index cc330ccdde..40768235b5 100644 --- a/services/github/model-github/src/plugin.ts +++ b/services/github/model-github/src/plugin.ts @@ -48,6 +48,7 @@ export default mergeIds(githubId, github, { PRClosedAt: '' as IntlString, MergeCommitSHA: '' as IntlString, GithubIssue: '' as IntlString, + GithubMilestone: '' as IntlString, Mergeable: '' as IntlString, GithubUser: '' as IntlString, diff --git a/services/github/pod-github/src/config.ts b/services/github/pod-github/src/config.ts index 88c806d7a1..3be2097fc3 100644 --- a/services/github/pod-github/src/config.ts +++ b/services/github/pod-github/src/config.ts @@ -27,6 +27,8 @@ interface Config { SentryDSN: string BrandingPath: string + + WorkspaceInactivityInterval: number // Interval in days to stop workspace synchronization if not visited } const envMap: { [key in keyof Config]: string } = { @@ -51,7 +53,9 @@ const envMap: { [key in keyof Config]: string } = { CollaboratorURL: 'COLLABORATOR_URL', SentryDSN: 'SENTRY_DSN', - BrandingPath: 'BRANDING_PATH' + BrandingPath: 'BRANDING_PATH', + + WorkspaceInactivityInterval: 'WORKSPACE_INACTIVITY_INTERVAL' } const required: Array = [ @@ -96,7 +100,8 @@ const config: Config = (() => { CollaboratorURL: process.env[envMap.CollaboratorURL], SentryDSN: process.env[envMap.SentryDSN], - BrandingPath: process.env[envMap.BrandingPath] ?? '' + BrandingPath: process.env[envMap.BrandingPath] ?? '', + WorkspaceInactivityInterval: parseInt(process.env[envMap.WorkspaceInactivityInterval] ?? '5') // In days } const missingEnv = required.filter((key) => params[key] === undefined).map((key) => envMap[key]) diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts index 7820768f40..b80dd8de88 100644 --- a/services/github/pod-github/src/platform.ts +++ b/services/github/pod-github/src/platform.ts @@ -697,6 +697,35 @@ export class PlatformWorker { return Array.from(workspaces) } + async checkWorkspaceIsActive (token: string, workspace: string): Promise { + let workspaceInfo: ClientWorkspaceInfo | undefined + try { + workspaceInfo = await getWorkspaceInfo(token) + } catch (err: any) { + this.ctx.error('Workspace not found:', { workspace }) + return + } + if (workspaceInfo?.workspace === undefined) { + this.ctx.error('No workspace exists for workspaceId', { workspace }) + return + } + if (!isActiveMode(workspaceInfo?.mode)) { + this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace }) + return + } + if (workspaceInfo?.disabled === true) { + this.ctx.warn('Workspace is disabled', { workspace }) + return + } + const lastVisit = (Date.now() - workspaceInfo.lastVisit) / (3600 * 24 * 1000) // In days + + if (config.WorkspaceInactivityInterval > 0 && lastVisit > config.WorkspaceInactivityInterval) { + this.ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace }) + return + } + return workspaceInfo + } + private async checkWorkspaces (): Promise { this.ctx.info('************************* Check workspaces ************************* ', { workspaces: this.clients.size @@ -737,27 +766,11 @@ export class PlatformWorker { }, { mode: 'github' } ) - let workspaceInfo: ClientWorkspaceInfo | undefined - try { - workspaceInfo = await getWorkspaceInfo(token, true) - } catch (err: any) { - this.ctx.error('Workspace not found:', { workspace }) + const workspaceInfo = await this.checkWorkspaceIsActive(token, workspace) + if (workspaceInfo === undefined) { errors++ return } - if (workspaceInfo?.workspace === undefined) { - this.ctx.error('No workspace exists for workspaceId', { workspace }) - errors++ - return - } - if (!isActiveMode(workspaceInfo?.mode)) { - this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace }) - return - } - if (workspaceInfo?.disabled === true) { - this.ctx.warn('Workspace is disabled', { workspace }) - return - } try { const branding = Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) ?? null const workerCtx = this.ctx.newChild('worker', { workspace: workspaceInfo.workspace }, {}) @@ -786,6 +799,14 @@ export class PlatformWorker { if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) { void this.clients.get(workspace)?.refreshClient(event === ClientConnectEvent.Upgraded) } + // We need to check if workspace is inactive + void this.checkWorkspaceIsActive(token, workspace).then((res) => { + if (res === undefined) { + this.ctx.warn('Workspace is inactive, removing from clients list.', { workspace }) + this.clients.delete(workspace) + void worker?.close() + } + }) } ) if (worker !== undefined) { diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts index c767ad9a9c..b0eeda0514 100644 --- a/services/github/pod-github/src/sync/issueBase.ts +++ b/services/github/pod-github/src/sync/issueBase.ts @@ -35,7 +35,8 @@ import github, { } from '@hcengineering/github' import { IntlString } from '@hcengineering/platform' import { LiveQuery } from '@hcengineering/query' -import task, { TaskType } from '@hcengineering/task' +import { getPublicLink } from '@hcengineering/server-guest-resources' +import task, { TaskType, type Task } from '@hcengineering/task' import { MarkupNode, MarkupNodeType, areEqualMarkups, markupToJSON, traverseNode } from '@hcengineering/text' import time, { type ToDo } from '@hcengineering/time' import tracker, { Issue, IssuePriority } from '@hcengineering/tracker' @@ -59,7 +60,7 @@ import { projectValue, supportedGithubTypes } from './githubTypes' -import { appendGuestLink, stripGuestLink } from './guest' +import { stripGuestLink } from './guest' import { syncConfig } from './syncConfig' import { collectUpdate, @@ -998,13 +999,7 @@ export abstract class IssueSyncManagerBase { } if (platformUpdate.description != null) { // Need to convert to markdown - const pp = async (nodes: MarkupNode): Promise => { - await appendGuestLink(this.client, doc, nodes, this.provider.getWorkspaceId(), this.provider.getBranding()) - } - issueUpdate.body = await this.provider.getMarkdown( - platformUpdate.description ?? '', - info.allowOpenInHuly === true ? pp : undefined - ) + issueUpdate.body = await this.provider.getMarkdown(platformUpdate.description ?? '') issueData.description = await this.provider.getMarkup( container.container, issueUpdate.body ?? '', @@ -1270,7 +1265,7 @@ export abstract class IssueSyncManagerBase { if (existing !== undefined && deleteExisting) { const childItems = await derivedClient.findAll(github.class.DocSyncInfo, { - parentUrl: (issueExternal.url ?? '').toLowerCase() + parent: (issueExternal.url ?? '').toLowerCase() }) for (const u of childItems) { // We need just to clean all of them, since child's for issue are comments for now. @@ -1315,4 +1310,35 @@ export abstract class IssueSyncManagerBase { this.provider.sync() } } + + async addHulyLink ( + info: DocSyncInfo, + syncResult: DocumentUpdate, + object: Doc, + external: IssueExternalData, + container: ContainerFocus + ): Promise { + const repository = await this.provider.getRepositoryById(info.repository) + if (repository !== undefined) { + syncResult.addHulyLink = false + const publicLink = await getPublicLink( + object, + this.client, + this.provider.getWorkspaceId(), + false, + this.provider.getBranding() + ) + // We need to create comment on Github about issue is connected. + await container.container.octokit.rest.issues.createComment({ + owner: repository.owner?.login as string, + repo: repository.name, + issue_number: external.number, + + body: `

Connected to Huly®: ${(object as Task).identifier}

`, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } + } } diff --git a/services/github/pod-github/src/sync/issues.ts b/services/github/pod-github/src/sync/issues.ts index a2bc87b74c..2e988c8207 100644 --- a/services/github/pod-github/src/sync/issues.ts +++ b/services/github/pod-github/src/sync/issues.ts @@ -19,9 +19,9 @@ import core, { TxOperations, cutObjectArray, generateId, - makeDocCollabId, + makeCollabId, makeCollabJsonId, - makeCollabId + makeDocCollabId } from '@hcengineering/core' import github, { DocSyncInfo, @@ -46,7 +46,6 @@ import { githubSyncVersion } from '../types' import { IssueExternalData, issueDetails } from './githubTypes' -import { appendGuestLink } from './guest' import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget, IssueUpdate, WithMarkup } from './issueBase' import { syncConfig } from './syncConfig' import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils' @@ -307,7 +306,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan external: issueExternal, externalVersion: githubExternalSyncVersion, lastModified: new Date(issueExternal.updatedAt).getTime(), - allowOpenInHuly: true + addHulyLink: true }) // We need trigger comments, if their sync data created before @@ -332,6 +331,9 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan if (container?.container === undefined) { return { needSync: githubSyncVersion } } + + let needCreateConnectedAtHuly = info.addHulyLink === true + if ( (container.project.projectNodeId === undefined || !container.container.projectStructure.has(container.project._id)) && @@ -413,12 +415,13 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan url: issueExternal.url.toLowerCase(), githubNumber: issueExternal.number, lastModified: new Date(issueExternal.updatedAt).getTime(), - allowOpenInHuly: true, + addHulyLink: false, // Do not need, since we create comment on Github about issue is connected. current: { title: issueExternal.title, description: await this.provider.getMarkup(container.container, issueExternal.body, this.stripGuestLink) } } + needCreateConnectedAtHuly = true await derivedClient.update(info, update) info.external = update.external info.externalVersion = update.externalVersion @@ -462,6 +465,11 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan } } } + + if (existing !== undefined && issueExternal !== undefined && needCreateConnectedAtHuly) { + await this.addHulyLink(info, syncResult, existing, issueExternal, container) + } + return { ...syncResult, issueExternal, @@ -837,10 +845,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan } }` - const body = - (await this.provider.getMarkdown(existingIssue.description, async (nodes) => { - await appendGuestLink(this.client, existing, nodes, this.provider.getWorkspaceId(), this.provider.getBranding()) - })) ?? '' + const body = (await this.provider.getMarkdown(existingIssue.description)) ?? '' if (isGHWriteAllowed()) { const response: | { diff --git a/services/github/pod-github/src/sync/projects.ts b/services/github/pod-github/src/sync/projects.ts index c6ca50ea9f..102211a404 100644 --- a/services/github/pod-github/src/sync/projects.ts +++ b/services/github/pod-github/src/sync/projects.ts @@ -33,6 +33,7 @@ import { ExternalSyncField, IntegrationContainer, IntegrationManager, + githubExternalSyncVersion, githubSyncVersion } from '../types' import { @@ -353,7 +354,16 @@ export class ProjectsSyncManager implements DocSyncManager { syncDocs: DocSyncInfo[], repository: GithubIntegrationRepository, project: GithubProject - ): Promise {} + ): Promise { + for (const d of syncDocs) { + if (d.objectClass === tracker.class.Milestone) { + // no external data for doc + await derivedClient.update(d, { + externalVersion: githubExternalSyncVersion + }) + } + } + } repositoryDisabled (integration: IntegrationContainer, repo: GithubIntegrationRepository): void { integration.synchronized.delete(`${repo._id}:issues`) diff --git a/services/github/pod-github/src/sync/pullrequests.ts b/services/github/pod-github/src/sync/pullrequests.ts index 33b8b77749..ee27c44a37 100644 --- a/services/github/pod-github/src/sync/pullrequests.ts +++ b/services/github/pod-github/src/sync/pullrequests.ts @@ -357,7 +357,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS external: pullRequestExternal, externalVersion: githubExternalSyncVersion, derivedVersion: '', - allowOpenInHuly: true, + addHulyLink: true, lastModified, lastGithubUser: account }) @@ -961,6 +961,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS if (container?.container === undefined) { return { needSync: githubSyncVersion } } + const needCreateConnectedAtHuly = info.addHulyLink === true if ( (container.project.projectNodeId === undefined || !container.container.projectStructure.has(container.project._id)) && @@ -991,6 +992,9 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS const syncResult = await this.syncToTarget(target, container, existing, pullRequestExternal, derivedClient, info) + if (existing !== undefined && pullRequestExternal !== undefined && needCreateConnectedAtHuly) { + await this.addHulyLink(info, syncResult, existing, pullRequestExternal, container) + } return { ...syncResult, targetNodeId: target.target.projectNodeId diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts index 6ffd45a024..5e25b64b61 100644 --- a/services/github/pod-github/src/worker.ts +++ b/services/github/pod-github/src/worker.ts @@ -1202,7 +1202,7 @@ export class GithubWorker implements IntegrationManager { const projects: GithubProject[] = [] const repositories: GithubIntegrationRepository[] = [] - const allProjects = await this.liveQuery.queryFind(github.mixin.GithubProject, {}) + const allProjects = await this.liveQuery.queryFind(github.mixin.GithubProject, { archived: false }) const allRepositories = await this.liveQuery.queryFind(github.class.GithubIntegrationRepository, { enabled: true }) for (const it of Array.from(this.integrations.values())) {