Remove unstable/unused Github project support (#8891)

* Remove unstable/unused Github project support

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Fix unused file

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

---------

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-05-12 16:37:39 +07:00
committed by GitHub
parent 94f8b55908
commit 8b91af8d8d
15 changed files with 37 additions and 2486 deletions
@@ -28,7 +28,6 @@ import { collectUpdate, deleteObjects, errorToObj, getSince, isGHWriteAllowed }
import { Analytics } from '@hcengineering/analytics'
import { IssueComment, IssueCommentCreatedEvent, IssueCommentEvent } from '@octokit/webhooks-types'
import config from '../config'
import { syncConfig } from './syncConfig'
interface MessageData {
message: string
@@ -88,14 +87,6 @@ export class CommentSyncManager implements DocSyncManager {
if (container === undefined) {
return false
}
if (
container?.container === undefined ||
((container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject)
) {
return false
}
const commentExternal = info.external as CommentExternalData | undefined
@@ -6,159 +6,6 @@ import {
PullRequestMergeable
} from '@hcengineering/github'
/**
* @public
*/
export type GithubDataType = 'SINGLE_SELECT' | 'TEXT' | 'DATE' | 'NUMBER'
/**
* @public
*/
export interface GithubProjectV2 {
projectV2: {
id: string
title: string
description: string
updatedAt: string
fields: {
edges: GithubProjectV2Field[]
}
}
}
/**
* @public
*/
export interface GithubProjectV2FieldOption {
name: string
color: string
description: string
id: string
}
/**
* @public
*/
export interface GithubProjectV2Field {
node: {
dataType: GithubDataType
updatedAt: string
id: string
name: string
options?: GithubProjectV2FieldOption[]
} & Record<string, any>
}
/**
* @public
*/
export interface GithubProjectV2ItemFieldValue {
id: string
// Date
date?: string
// Number
number?: number
// Single select
color?: string
description?: string
optionId?: string
// Text
text?: string
field: {
id: string
name: string
dataType: GithubDataType
}
}
export interface GithubProjectV2Item {
id: string
type: 'ISSUE' | 'PULL_REQUEST' | 'DRAFT_ISSUE' | 'REDACTED'
project: {
id: string
number: number
}
fieldValues: {
nodes: (GithubProjectV2ItemFieldValue | any)[]
}
}
export const projectV2Field = `
... on ProjectV2Field {
id
name
updatedAt
dataType
}
... on ProjectV2IterationField {
id
name
dataType
updatedAt
}
... on ProjectV2SingleSelectField {
id
name
options {
name
id
color
description
}
dataType
updatedAt
}
`
export const projectV2ItemFields = `
... on ProjectV2ItemFieldDateValue {
id
date
field {
... on ProjectV2Field {
id
name
dataType
}
}
}
... on ProjectV2ItemFieldNumberValue {
id
number
field {
... on ProjectV2Field {
id
name
dataType
}
}
}
... on ProjectV2ItemFieldSingleSelectValue {
id
name
color
description
optionId
field {
... on ProjectV2SingleSelectField {
id
name
dataType
}
}
}
... on ProjectV2ItemFieldTextValue {
id
text
field {
... on ProjectV2Field {
id
name
dataType
}
}
}
`
export const assigneesField = `
assignees(first: 10) {
nodes {
@@ -245,22 +92,6 @@ title
updatedAt
url
${reactionsField}
projectItems(first: 10, includeArchived: true) {
nodes {
id
type
project {
id
url
number
}
fieldValues(first: 50) {
nodes {
${projectV2ItemFields}
}
}
}
}
lastEditedAt
publishedAt
`
@@ -307,9 +138,6 @@ export interface IssueExternalData {
}
}[]
}
projectItems: {
nodes: GithubProjectV2Item[]
}
lastEditedAt: string
publishedAt: string
}
+14 -657
View File
@@ -21,57 +21,23 @@ import core, {
PersonId,
Ref,
Space,
Status,
TxOperations,
makeDocCollabId
} from '@hcengineering/core'
import github, {
DocSyncInfo,
GithubFieldMapping,
GithubIntegrationRepository,
GithubIssue,
GithubMilestone,
GithubProject
} from '@hcengineering/github'
import github, { DocSyncInfo, GithubIntegrationRepository, GithubIssue, GithubProject } from '@hcengineering/github'
import { IntlString } from '@hcengineering/platform'
import { LiveQuery } from '@hcengineering/query'
import { getPublicLink } from '@hcengineering/server-guest-resources'
import task, { TaskType, type Task } from '@hcengineering/task'
import task, { 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'
import { ProjectsV2ItemEvent } from '@octokit/webhooks-types'
import tracker, { Issue } from '@hcengineering/tracker'
import { deepEqual } from 'fast-equals'
import { Octokit } from 'octokit'
import {
ContainerFocus,
IntegrationContainer,
IntegrationManager,
githubExternalSyncVersion,
githubSyncVersion
} from '../types'
import {
GithubDataType,
GithubProjectV2FieldOption,
GithubProjectV2Item,
GithubProjectV2ItemFieldValue,
IssueExternalData,
fieldValues,
projectValue,
supportedGithubTypes
} from './githubTypes'
import { ContainerFocus, IntegrationManager, githubExternalSyncVersion, githubSyncVersion } from '../types'
import { IssueExternalData } from './githubTypes'
import { stripGuestLink } from './guest'
import { syncConfig } from './syncConfig'
import {
collectUpdate,
compareMarkdown,
deleteObjects,
errorToObj,
getCreateStatus,
getType,
guessStatus,
isGHWriteAllowed
} from './utils'
import { collectUpdate, compareMarkdown, deleteObjects, errorToObj, guessStatus } from './utils'
/**
* @public
@@ -116,16 +82,6 @@ Record<string, any>
*/
export type IssueUpdate = DocumentUpdate<WithMarkup<Issue>>
/**
* @public
*/
export interface IssueSyncTarget {
project: GithubProject
mappings: GithubFieldMapping[]
target: GithubProject | GithubMilestone
prjData?: GithubProjectV2Item
}
export abstract class IssueSyncManagerBase {
provider!: IntegrationManager
constructor (
@@ -157,189 +113,6 @@ export abstract class IssueSyncManagerBase {
return socialIds.map((it) => it.attachedTo)
}
async processProjectV2Event (
integration: IntegrationContainer,
event: ProjectsV2ItemEvent,
derivedClient: TxOperations,
prj: GithubProject
): Promise<void> {
const account = (await this.provider.getAccountU(event.sender)) ?? core.account.System
switch (event.action) {
case 'edited': {
const itemId = event.projects_v2_item.node_id
const projectId = event.projects_v2_item.project_node_id
try {
const actualContent: {
node: {
id: string
content: {
id: string
url: string
number: number
}
fieldValues: {
nodes: GithubProjectV2ItemFieldValue[]
}
}
} = (await integration.octokit?.graphql(
`query listIssue($nodeId: ID!) {
node(id: $nodeId) {
... on ProjectV2Item {
id
content {
... on Issue {
id
number
url
}
... on PullRequest {
id
number
url
}
}
${fieldValues}
}
}
}`,
{
nodeId: itemId
}
)) as any
const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: prj._id,
url: (actualContent.node.content.url ?? '').toLowerCase()
})
if (syncData !== undefined) {
const milestone = await this.client.findOne<GithubMilestone>(github.mixin.GithubMilestone, {
projectNodeId: projectId
})
const target: IssueSyncTarget | undefined =
milestone !== undefined
? {
mappings: milestone.mappings ?? [],
project: prj,
target: milestone
}
: prj.projectNodeId === projectId
? this.getProjectIssueTarget(prj)
: undefined
if (target === undefined) {
// Not our project, we should just update external
return
}
this.ctx.info('event for issue', { url: syncData.url, workspace: this.provider.getWorkspaceId() })
const externalData = syncData.external as IssueExternalData
// We need to replace field values we retrieved
target.prjData = externalData.projectItems.nodes.find(
(it) => it.project.id === event.projects_v2_item.project_node_id
)
if (target.prjData === undefined) {
target.prjData = {
fieldValues: actualContent.node.fieldValues,
id: event.projects_v2_item.node_id,
type: 'ISSUE',
project: {
id: prj.projectNodeId as string,
number: prj.projectNumber as number
}
}
externalData.projectItems.nodes.push(target.prjData)
} else {
target.prjData.fieldValues = actualContent.node.fieldValues
}
// Store github values
await derivedClient.update(syncData, {
external: externalData,
externalVersion: githubExternalSyncVersion
})
if (event.changes.field_value === undefined) {
this.ctx.info('No changes for change event', { event, workspace: this.provider.getWorkspaceId() })
return
}
let needProjectRefresh = false
const update: DocumentUpdate<WithMarkup<Issue>> & Record<string, any> = {}
let structure = integration.projectStructure.get(target.target._id)
for (const f of target.prjData.fieldValues?.nodes ?? []) {
if (!('id' in f)) {
continue
}
// Check if we need to update project structure
if (structure !== undefined) {
const ff = structure.projectV2.fields.edges.find((it) => it.node.id === f.field.id)
if (ff === undefined && supportedGithubTypes.has(f.field.dataType)) {
// We have missing field.
needProjectRefresh = true
}
}
if (needProjectRefresh && syncData.repository != null) {
const repo = await this.provider.liveQuery.findOne<GithubIntegrationRepository>(
github.class.GithubIntegrationRepository,
{
_id: syncData.repository
}
)
if (repo !== undefined) {
await this.provider.handleEvent(github.class.GithubIntegration, integration.installationId, repo, {})
structure = integration.projectStructure.get(prj._id)
}
}
const taskTypes = (
await this.provider.liveQuery.queryFind(task.class.TaskType, { parent: prj.type })
).filter((it) => this.client.getHierarchy().isDerived(it.targetClass, syncData.objectClass))
// TODO: Use GithubProject configuration to specify target type for issues
if (taskTypes.length === 0) {
// Missing required task type
this.ctx.error('Missing required task type, in Event.')
}
if (event.changes.field_value.field_node_id === f.field.id && taskTypes.length > 0) {
const ff = await this.toPlatformField(
{
container: integration,
project: prj
},
f,
target,
taskTypes[0]
)
if (ff === undefined) {
continue
}
const { value, mapping } = ff
if (value !== undefined) {
update[mapping.name] = value
}
continue
}
}
if (Object.keys(update).length > 0) {
await this.handleUpdate(externalData, derivedClient, update, account, prj, false, syncData)
}
}
} catch (err: any) {
Analytics.handleError(err)
this.ctx.error(err, event)
}
break
}
}
}
async handleUpdate (
external: IssueExternalData,
derivedClient: TxOperations,
@@ -425,12 +198,7 @@ export abstract class IssueSyncManagerBase {
},
lastModified
)
await this.client.diffUpdate(
this.client.getHierarchy().as(doc, prj.mixinClass),
issueData,
lastModified,
account
)
await this.client.diffUpdate(doc, issueData, lastModified, account)
this.provider.sync()
}
}
@@ -439,229 +207,6 @@ export abstract class IssueSyncManagerBase {
}
}
async addIssueToProject (
container: ContainerFocus,
okit: Octokit,
issue: IssueExternalData,
projectTarget: string
): Promise<GithubProjectV2Item | undefined> {
const query = `mutation addIssueToProject($project: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $contentId}) {
item {
${projectValue}
id
type
${fieldValues}
}
}
}`
if (isGHWriteAllowed()) {
const response: any = await okit.graphql(query, { project: projectTarget, contentId: issue.id })
return response.addProjectV2ItemById.item
}
}
async removeIssueFromProject (okit: Octokit, projectTarget: string, issueId: string): Promise<void> {
try {
const query = `mutation removeIssueToProject($project: ID!, $contentId: ID!) {
deleteProjectV2Item(input: {projectId: $project, itemId: $contentId}) {
deletedItemId
}
}`
if (isGHWriteAllowed()) {
await okit.graphql(query, { project: projectTarget, contentId: issueId })
}
} catch (err: any) {
Analytics.handleError(err)
this.ctx.error(err)
}
}
findOption (
container: ContainerFocus,
field: GithubProjectV2ItemFieldValue,
target: GithubProject | GithubMilestone
): GithubProjectV2FieldOption | undefined {
const structure = container.container.projectStructure.get(target._id)
if (structure === undefined) {
return {
id: field.field.id,
name: field.field.name,
color: field.color ?? '',
description: field.description ?? ''
}
}
const pField = structure.projectV2.fields.edges.find((it) => it.node.id === field.field.id)
if (pField === undefined) {
return {
id: field.field.id,
name: field.field.name,
color: field.color ?? '',
description: field.description ?? ''
}
}
return (pField.node.options ?? []).find((it) => it.id === field.optionId)
}
findOptionId (
container: ContainerFocus,
fieldId: string,
value: string | null,
target: IssueSyncTarget
): string | undefined {
if (value == null) {
return
}
const structure = container.container.projectStructure.get(target.target._id)
if (structure === undefined) {
return
}
const pField = structure.projectV2.fields.edges.find((it) => it.node.id === fieldId)
if (pField === undefined) {
return undefined
}
return (pField.node.options ?? []).find((it) => it.name?.toLowerCase() === value.toLowerCase())?.id
}
async toPlatformField (
container: ContainerFocus,
// eslint-disable-next-line @typescript-eslint/ban-types
field: GithubProjectV2ItemFieldValue | {},
target: IssueSyncTarget,
taskType: TaskType
): Promise<{ value: any, mapping: GithubFieldMapping } | undefined> {
if (!('field' in field)) {
return
}
const mapping = target.mappings.find((it) => it.githubId === field.field.id)
if (mapping === undefined) {
return undefined
}
if (mapping.name === 'status') {
const option = this.findOption(container, field, target.target)
if (option === undefined) {
return
}
return {
value: await getCreateStatus(
this.ctx,
this.provider,
this.client,
container.project,
option?.name,
option.description,
option.color,
taskType
),
mapping
}
}
if (mapping.name === 'priority') {
const values: Record<string, IssuePriority> = {
'': IssuePriority.NoPriority,
High: IssuePriority.High,
Medium: IssuePriority.Medium,
Low: IssuePriority.Low,
Urgent: IssuePriority.Urgent
}
const option = this.findOption(container, field, target.target)
return { value: values[option?.name ?? ''] ?? IssuePriority.NoPriority, mapping }
}
switch (field.field.dataType) {
case 'DATE':
return { value: field.date !== undefined ? new Date(field.date).getTime() : null, mapping }
case 'NUMBER':
return { value: field.number, mapping }
case 'TEXT':
return { value: field.text, mapping }
case 'SINGLE_SELECT': {
const option = this.findOption(container, field, target.target)
return { value: option?.name, mapping }
}
}
}
async fillProjectV2Fields (
target: IssueSyncTarget,
container: ContainerFocus,
issueData: Record<string, any>,
taskType: TaskType
): Promise<void> {
for (const f of target.prjData?.fieldValues?.nodes ?? []) {
const ff = await this.toPlatformField(container, f, target, taskType)
if (ff === undefined) {
continue
}
const { value, mapping } = ff
if (value !== undefined) {
;(issueData as any)[mapping.name] = value
}
}
}
async updateIssueValues (
target: IssueSyncTarget,
okit: Octokit,
values: { id: string, value: any, dataType: GithubDataType }[]
): Promise<{ error: any, response: any }[]> {
function getValue (val: { id: string, value: any, dataType: GithubDataType }): string {
switch (val.dataType) {
case 'SINGLE_SELECT':
return `singleSelectOptionId: "${val.value as string}"`
case 'DATE':
return `date: "${new Date(val.value).toISOString()}"`
case 'NUMBER':
return `number: ${val.value as number}`
case 'TEXT':
return `text: "${val.value as string}"`
}
}
const errors: any[] = []
const itm = ` {
projectV2Item {
id
type
${projectValue}
${fieldValues}
}
}\n`
let response: any = {}
if (isGHWriteAllowed()) {
for (const val of values) {
const q = `
mutation updateField($project: ID!, $itemId: ID!) {
updateProjectV2ItemFieldValue(
input: {projectId: $project, itemId: $itemId, fieldId: "${val.id}", value: {${getValue(val)}}}
)
${itm}
}`
try {
response = await okit.graphql(q, {
project: target.target.projectNodeId,
itemId: target.prjData?.id as string
})
} catch (err: any) {
if (err.errors?.[0]?.type === 'NOT_FOUND') {
errors.push({ error: err, response })
return errors
}
Analytics.handleError(err)
// Failed to update one particular value, skip it.
this.ctx.error('error during field update', {
error: err,
response,
workspace: this.provider.getWorkspaceId()
})
errors.push({ error: err, response })
}
}
}
return errors
}
abstract fillBackChanges (update: DocumentUpdate<Issue>, existing: GithubIssue, external: any): Promise<void>
async addConnectToMessage (
@@ -702,15 +247,13 @@ export abstract class IssueSyncManagerBase {
abstract afterSync (existing: Issue, account: PersonId, issueExternal: any, info: DocSyncInfo): Promise<void>
async handleDiffUpdate (
target: IssueSyncTarget,
container: ContainerFocus,
existing: WithMarkup<Issue>,
info: DocSyncInfo,
issueData: GithubIssueData,
container: ContainerFocus,
issueExternal: IssueExternalData,
account: PersonId,
accountGH: PersonId,
syncToProject: boolean
accountGH: PersonId
): Promise<DocumentUpdate<DocSyncInfo>> {
let needUpdate = false
if (!this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) {
@@ -746,35 +289,18 @@ export abstract class IssueSyncManagerBase {
await this.notifyConnected(container, info, existing, issueExternal)
}
}
if (!this.client.getHierarchy().hasMixin(existing, container.project.mixinClass)) {
await this.ctx.withLog(
'create mixin issue',
{},
() =>
this.client.createMixin<Issue, Issue>(
existing._id as Ref<GithubIssue>,
existing._class,
existing.space,
container.project.mixinClass,
{}
),
{ identifier: existing.identifier, url: issueExternal.url }
)
// Re iterate to have existing value with mixin inside.
needUpdate = true
}
if (needUpdate) {
return { needSync: '' }
}
const existingIssue = this.client.getHierarchy().as(existing, container.project.mixinClass)
const existingIssue = this.client.getHierarchy().as(existing, github.mixin.GithubIssue)
const previousData: GithubIssueData = info.current ?? ({} as unknown as GithubIssueData)
const type = await this.provider.getTaskTypeOf(container.project.type, existing._class)
const stst = await this.provider.getStatuses(type?._id)
// const type = await this.provider.getTaskTypeOf(container.project.type, existing._class)
// const stst = await this.provider.getStatuses(type?._id)
const update = collectUpdate<Issue>(previousData, issueData, Object.keys(issueData))
const allAttributes = this.client.getHierarchy().getAllAttributes(container.project.mixinClass)
const allAttributes = this.client.getHierarchy().getAllAttributes(existingIssue._class)
const platformUpdate = collectUpdate<Issue>(previousData, existingIssue, Array.from(allAttributes.keys()))
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
@@ -826,107 +352,12 @@ export abstract class IssueSyncManagerBase {
okit,
account
)
const fieldsUpdate: { id: string, value: any, dataType: GithubDataType }[] = []
// Collect field update.
for (const [k, v] of Object.entries(platformUpdate)) {
const mapping = target.mappings.filter((it) => it != null).find((it) => it.name === k)
if (mapping === undefined) {
continue
}
const attr = this.client.getHierarchy().getAttribute(mapping._class, mapping.name)
if (attr.name === 'status') {
// Handle status field
const status = stst.find((it) => it._id === v) as Status
const optionId = this.findOptionId(container, mapping.githubId, status.name, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(' => prepare issue status update', {
url: issueExternal.url,
name: status.name,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
if (attr.name === 'priority') {
const values: Record<IssuePriority, string> = {
[IssuePriority.NoPriority]: '',
[IssuePriority.High]: 'High',
[IssuePriority.Medium]: 'Medium',
[IssuePriority.Low]: 'Low',
[IssuePriority.Urgent]: 'Urgent'
}
// Handle priority field TODO: Add clear of field
const priorityName = values[v as IssuePriority]
const optionId = this.findOptionId(container, mapping.githubId, priorityName, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(' => prepare issue priority update', {
url: issueExternal.url,
priority: priorityName,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
const dataType = getType(attr)
if (dataType === 'SINGLE_SELECT') {
// Handle status field
const optionId = this.findOptionId(container, mapping.githubId, v, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(` => prepare issue field ${attr.label} update`, {
url: issueExternal.url,
value: v,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
if (dataType === undefined) {
continue
}
fieldsUpdate.push({
id: mapping.githubId,
dataType,
value: v
})
this.ctx.info(`=> prepare issue field ${attr.label} update`, {
url: issueExternal.url,
value: v,
workspace: this.provider.getWorkspaceId()
})
}
if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) {
const errors = await this.updateIssueValues(target, okit, fieldsUpdate)
if (errors.length === 0) {
needExternalSync = true
}
}
// TODO: Add support for labels, milestone, assignees
}
// We need remove all readonly field values
for (const k of Object.keys(update)) {
// Skip readonly fields
const attr = this.client.getHierarchy().findAttribute(target.project.mixinClass, k)
const attr = this.client.getHierarchy().findAttribute(existingIssue._class, k)
if (attr?.readonly === true) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (update as any)[k]
@@ -1214,40 +645,6 @@ export abstract class IssueSyncManagerBase {
this.provider.sync()
}
async getMilestoneIssueTarget (
project: GithubProject,
container: IntegrationContainer,
existingIssue: Issue | undefined,
external: IssueExternalData
): Promise<IssueSyncTarget | undefined> {
if (existingIssue !== undefined) {
// Select a milestone project
if (existingIssue.milestone != null) {
const milestone = await this.provider.liveQuery.findOne<GithubMilestone>(github.mixin.GithubMilestone, {
_id: existingIssue.milestone as Ref<GithubMilestone>
})
if (milestone === undefined) {
return
}
return {
project,
mappings: milestone.mappings ?? [],
target: milestone,
prjData: external.projectItems.nodes.find((it) => it.project.id === milestone.projectNodeId)
}
}
}
}
getProjectIssueTarget (project: GithubProject, external?: IssueExternalData): IssueSyncTarget {
return {
project,
mappings: project.mappings ?? [],
target: project,
prjData: external?.projectItems.nodes.find((it) => it.project.id === project.projectNodeId)
}
}
abstract deleteGithubDocument (container: ContainerFocus, account: PersonId, id: string): Promise<void>
async handleDelete (
@@ -1260,14 +657,6 @@ export abstract class IssueSyncManagerBase {
if (container === undefined) {
return false
}
if (
container?.container === undefined ||
((container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject)
) {
return false
}
const issueExternal = info.external as IssueExternalData | undefined
@@ -1276,38 +665,6 @@ export abstract class IssueSyncManagerBase {
return true
}
const account = existing?.createdBy ?? (await this.provider.getAccount(issueExternal.author)) ?? core.account.System
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
if (existing !== undefined && issueExternal !== undefined) {
let target = await this.getMilestoneIssueTarget(
container.project,
container.container,
existing as Issue,
issueExternal
)
if (target === null) {
// We need to wait, no milestone data yet.
return false
}
if (target === undefined) {
target = this.getProjectIssueTarget(container.project, issueExternal)
}
const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId
const supportProjects =
(isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones)
// A target node id
const targetNodeId: string | undefined = info.targetNodeId as string
if (targetNodeId !== undefined && supportProjects) {
const itemNode = issueExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId)
if (itemNode !== undefined) {
await this.removeIssueFromProject(okit, targetNodeId, itemNode.id)
}
// Clear external project items
info.external.projectItems = []
}
}
if (issueExternal !== undefined) {
try {
+6 -115
View File
@@ -45,8 +45,7 @@ import {
githubSyncVersion
} from '../types'
import { IssueExternalData, issueDetails } from './githubTypes'
import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget, IssueUpdate, WithMarkup } from './issueBase'
import { syncConfig } from './syncConfig'
import { GithubIssueData, IssueSyncManagerBase, IssueUpdate, WithMarkup } from './issueBase'
import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils'
export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncManager {
@@ -92,35 +91,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
const projectV2Event = (event as ProjectsV2ItemEvent).projects_v2_item?.id !== undefined
if (projectV2Event) {
const projectV2Event = event as ProjectsV2ItemEvent
const githubProjects = await this.provider.liveQuery.findAll(github.mixin.GithubProject, {
archived: false
})
let prj = githubProjects.find((it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id)
if (prj === undefined) {
// Checking for milestones
const m = await this.provider.liveQuery.findOne(github.mixin.GithubMilestone, {
projectNodeId: projectV2Event.projects_v2_item.project_node_id
})
if (m !== undefined) {
prj = githubProjects.find((it) => it._id === m.space)
}
}
if (prj === undefined) {
this.ctx.info('Event from unknown v2 project', {
nodeId: projectV2Event.projects_v2_item.project_node_id,
workspace: this.provider.getWorkspaceId()
})
return
}
const urlId = projectV2Event.projects_v2_item.node_id
await syncRunner.exec(urlId, async () => {
await this.processProjectV2Event(integration, projectV2Event, derivedClient, prj as GithubProject)
})
// Just ignore
} else {
const issueEvent = event as IssuesEvent
const { project, repository } = await this.provider.getProjectAndRepository(issueEvent.repository.node_id)
@@ -336,15 +307,6 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
let needCreateConnectedAtHuly = info.addHulyLink === true
if (
(container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject
) {
this.ctx.error('Not syncing no structure', { url: info.url })
return { needSync: '' }
}
if (info.repository == null && existing !== undefined) {
if (this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) {
const repositoryId = this.client.getHierarchy().as(existing, github.mixin.GithubIssue).repository
@@ -428,17 +390,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
return { needSync: githubSyncVersion }
}
let target = await this.getMilestoneIssueTarget(
container.project,
container.container,
existing as Issue,
issueExternal
)
if (target === undefined) {
target = this.getProjectIssueTarget(container.project, issueExternal)
}
const syncResult = await this.syncToTarget(target, container, existing, issueExternal, derivedClient, info)
const syncResult = await this.syncToTarget(container, existing, issueExternal, derivedClient, info)
if (externalWasCreated && existing !== undefined) {
// Create child documents
@@ -466,13 +418,11 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
return {
...syncResult,
issueExternal,
targetNodeId: target.target.projectNodeId
issueExternal
}
}
async syncToTarget (
target: IssueSyncTarget,
container: ContainerFocus,
existing: Doc | undefined,
issueExternal: IssueExternalData,
@@ -484,15 +434,6 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
const accountGH =
info.lastGithubUser ?? (await this.provider.getAccount(issueExternal.author)) ?? core.account.System
const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId
const supportProjects =
(isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones)
// A target node id
const targetNodeId: string | undefined = info.targetNodeId as string
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
const type = await this.provider.getTaskTypeOf(container.project.type, tracker.class.Issue)
const statuses = await this.provider.getStatuses(type?._id)
// collaborators: assignees.map((it) => it._id),
@@ -519,53 +460,6 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
return { needSync: githubSyncVersion }
}
await this.fillProjectV2Fields(target, container, issueData, taskTypes[0])
if (
targetNodeId !== undefined &&
target.target.projectNodeId !== undefined &&
targetNodeId !== target.target.projectNodeId &&
supportProjects
) {
const itemNode = issueExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId)
if (itemNode !== undefined) {
await this.removeIssueFromProject(okit, targetNodeId, itemNode.id)
// remove data
issueExternal.projectItems.nodes = issueExternal.projectItems.nodes.filter((it) => it.id !== targetNodeId)
target.prjData = undefined
await derivedClient.update(info, {
external: issueExternal,
externalVersion: githubExternalSyncVersion
})
// We need to sync from platform as new to new project.
// We need to remove current sync
info.current = {}
}
}
if (target.prjData === undefined && okit !== undefined && syncConfig.IssuesInProject && supportProjects) {
try {
this.ctx.info('add issue to project v2', {
url: issueExternal.url,
workspace: this.provider.getWorkspaceId()
})
target.prjData = await this.ctx.withLog('add issue to project v2', {}, () =>
this.addIssueToProject(container, okit, issueExternal, target.target.projectNodeId as string)
)
if (target.prjData !== undefined) {
issueExternal.projectItems.nodes.push(target.prjData)
}
await derivedClient.update(info, {
external: issueExternal,
externalVersion: githubExternalSyncVersion
})
} catch (err: any) {
Analytics.handleError(err)
this.ctx.error('Error add project v2', { err })
return { needSync: githubSyncVersion, error: JSON.stringify(err) }
}
}
if (existing === undefined) {
try {
this.ctx.info('create platform issue', {
@@ -639,15 +533,13 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
{},
async () =>
await this.handleDiffUpdate(
target,
container,
{ ...(existing as any), description },
info,
issueData,
container,
issueExternal,
account,
accountGH,
supportProjects
accountGH
),
{ url: issueExternal.url }
)
@@ -954,7 +846,6 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
repository: repo,
descriptionLocked: isDescriptionLocked
})
await this.client.createMixin<Issue, Issue>(issueId, github.mixin.GithubIssue, prj._id, prj.mixinClass, {})
await this.addConnectToMessage(
github.string.IssueConnectedActivityInfo,
File diff suppressed because it is too large Load Diff
@@ -57,8 +57,7 @@ import {
toReviewDecision,
toReviewState
} from './githubTypes'
import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget, WithMarkup } from './issueBase'
import { syncConfig } from './syncConfig'
import { GithubIssueData, IssueSyncManagerBase, WithMarkup } from './issueBase'
import {
errorToObj,
getSinceRaw,
@@ -99,35 +98,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
const projectV2Event = (_event as any as ProjectsV2ItemEvent).projects_v2_item?.id !== undefined
if (projectV2Event) {
const projectV2Event = _event as ProjectsV2ItemEvent
const githubProjects = await this.provider.liveQuery.findAll(github.mixin.GithubProject, {
archived: false
})
let prj = githubProjects.find((it) => it.projectNodeId === projectV2Event.projects_v2_item.project_node_id)
if (prj === undefined) {
// Checking for milestones
const m = await this.provider.liveQuery.findOne(github.mixin.GithubMilestone, {
projectNodeId: projectV2Event.projects_v2_item.project_node_id
})
if (m !== undefined) {
prj = githubProjects.find((it) => it._id === m.space)
}
}
if (prj === undefined) {
this.ctx.info('Event from unknown v2 project', {
nodeId: projectV2Event.projects_v2_item.project_node_id,
workspace: this.provider.getWorkspaceId()
})
return
}
const urlId = projectV2Event.projects_v2_item.node_id
await syncRunner.exec(urlId, async () => {
await this.processProjectV2Event(integration, projectV2Event, derivedClient, prj as GithubProject)
})
// Ignore
} else {
const event = _event as PullRequestEvent
const { project, repository } = await this.provider.getProjectAndRepository(event.repository.node_id)
@@ -379,7 +350,6 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
}
async syncToTarget (
target: IssueSyncTarget,
container: ContainerFocus,
existing: Doc | undefined,
pullRequestExternal: PullRequestExternalData,
@@ -391,66 +361,9 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
const accountGH =
info.lastGithubUser ?? (await this.provider.getAccount(pullRequestExternal.author)) ?? core.account.System
// A target node id
const targetNodeId: string | undefined = info.targetNodeId as string
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
const isProjectProjectTarget = target.target.projectNodeId === target.project.projectNodeId
const supportProjects =
(isProjectProjectTarget && syncConfig.MainProject) || (!isProjectProjectTarget && syncConfig.SupportMilestones)
const type = await this.provider.getTaskTypeOf(container.project.type, github.class.GithubPullRequest)
const statuses = await this.provider.getStatuses(type?._id)
if (
targetNodeId !== undefined &&
target.target.projectNodeId !== undefined &&
targetNodeId !== target.target.projectNodeId &&
supportProjects
) {
const itemNode = pullRequestExternal.projectItems.nodes.find((it) => it.project.id === targetNodeId)
if (itemNode !== undefined) {
await this.removeIssueFromProject(okit, target.target.projectNodeId, itemNode.id)
// remove data
pullRequestExternal.projectItems.nodes = pullRequestExternal.projectItems.nodes.filter(
(it) => it.id !== targetNodeId
)
await derivedClient.update(info, {
external: pullRequestExternal,
externalVersion: githubExternalSyncVersion
})
target.prjData = undefined
// We need to sync from platform as new to new project.
// We need to remove current sync
info.current = {}
}
}
// Check if issue are added to project.
if (target.prjData === undefined && okit !== undefined && supportProjects) {
try {
target.prjData = await this.ctx.withLog(
'add pull request to project}',
{},
() => this.addIssueToProject(container, okit, pullRequestExternal, target.target.projectNodeId as string),
{ url: pullRequestExternal.url }
)
if (target.prjData !== undefined) {
pullRequestExternal.projectItems.nodes.push(target.prjData)
}
await derivedClient.update(info, {
external: pullRequestExternal,
externalVersion: githubExternalSyncVersion
})
} catch (err: any) {
this.ctx.error('Error', { err })
Analytics.handleError(err)
return { needSync: githubSyncVersion, error: errorToObj(err) }
}
}
const assignees = await this.getAssignees(pullRequestExternal)
const reviewers: PersonId[] = await this.getReviewers(pullRequestExternal)
@@ -497,7 +410,6 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
this.ctx.error('Missing required task type', { url: pullRequestExternal.url })
return { needSync: githubSyncVersion }
}
await this.fillProjectV2Fields(target, container, pullRequestData, taskTypes[0])
const lastModified = new Date(pullRequestExternal.updatedAt).getTime()
@@ -617,15 +529,13 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
{},
() =>
this.handleDiffUpdate(
target,
container,
{ ...(existing as any), description },
info,
pullRequestData,
container,
pullRequestExternal,
account,
accountGH,
supportProjects
accountGH
),
{ url: pullRequestExternal.url }
)
@@ -980,13 +890,6 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
return { needSync: githubSyncVersion }
}
const needCreateConnectedAtHuly = info.addHulyLink === true
if (
(container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject
) {
return { needSync: githubSyncVersion }
}
if (info.repository == null) {
return { needSync: githubSyncVersion }
@@ -998,24 +901,13 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
return { needSync: '' }
}
let target = await this.getMilestoneIssueTarget(
container.project,
container.container,
existing as Issue,
pullRequestExternal
)
if (target === undefined) {
target = this.getProjectIssueTarget(container.project, pullRequestExternal)
}
const syncResult = await this.syncToTarget(target, container, existing, pullRequestExternal, derivedClient, info)
const syncResult = await this.syncToTarget(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
...syncResult
}
}
@@ -1251,7 +1143,6 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
descriptionLocked: isDescriptionLocked
}
)
await client.createMixin<Issue, Issue>(prId, github.mixin.GithubIssue, prj._id, prj.mixinClass, {})
await this.addConnectToMessage(
github.string.PullRequestConnectedActivityInfo,
@@ -11,13 +11,13 @@ import core, {
Ref,
TxOperations
} from '@hcengineering/core'
import { LiveQuery } from '@hcengineering/query'
import github, {
DocSyncInfo,
GithubIntegrationRepository,
GithubProject,
GithubReviewComment
} from '@hcengineering/github'
import { LiveQuery } from '@hcengineering/query'
import {
ContainerFocus,
DocSyncManager,
@@ -33,7 +33,6 @@ import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed } from './ut
import { Analytics } from '@hcengineering/analytics'
import { PullRequestReviewCommentCreatedEvent, PullRequestReviewCommentEvent } from '@octokit/webhooks-types'
import config from '../config'
import { syncConfig } from './syncConfig'
export type ReviewCommentData = DocData<GithubReviewComment>
@@ -97,14 +96,6 @@ export class ReviewCommentSyncManager implements DocSyncManager {
if (container === undefined) {
return false
}
if (
container?.container === undefined ||
((container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject)
) {
return false
}
const commentExternal = info.external
@@ -39,7 +39,6 @@ import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed, syncChilds,
import { Analytics } from '@hcengineering/analytics'
import { PullRequestReviewThreadEvent } from '@octokit/webhooks-types'
import config from '../config'
import { syncConfig } from './syncConfig'
import { githubConfiguration } from './configuration'
export type ReviewThreadData = Pick<
@@ -116,14 +115,6 @@ export class ReviewThreadSyncManager implements DocSyncManager {
if (container === undefined) {
return false
}
if (
container?.container === undefined ||
((container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject)
) {
return false
}
const commentExternal = info.external
@@ -33,7 +33,6 @@ import { collectUpdate, deleteObjects, errorToObj, isGHWriteAllowed, syncChilds
import { Analytics } from '@hcengineering/analytics'
import { PullRequestReviewEvent, PullRequestReviewSubmittedEvent } from '@octokit/webhooks-types'
import config from '../config'
import { syncConfig } from './syncConfig'
export type ReviewData = Pick<GithubReview, 'body' | 'state' | 'comments'>
@@ -94,14 +93,6 @@ export class ReviewSyncManager implements DocSyncManager {
if (container === undefined) {
return false
}
if (
container?.container === undefined ||
((container.project.projectNodeId === undefined ||
!container.container.projectStructure.has(container.project._id)) &&
syncConfig.MainProject)
) {
return false
}
const commentExternal = info.external
@@ -1,10 +0,0 @@
/**
* @public
*/
export const syncConfig = {
MainProject: false,
SupportMilestones: true,
IssuesInProject: true,
BacklogInProject: false,
PullRequestsInProject: false
}
+5 -94
View File
@@ -1,20 +1,17 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
PersonId,
AnyAttribute,
AttachedDoc,
Class,
Doc,
DocumentQuery,
DocumentUpdate,
MeasureContext,
PersonId,
Ref,
SortingOrder,
Status,
Timestamp,
TxOperations,
Type,
toIdMap
TxOperations
} from '@hcengineering/core'
import github, {
DocSyncInfo,
@@ -23,11 +20,10 @@ import github, {
GithubProject
} from '@hcengineering/github'
import { PlatformError, unknownStatus } from '@hcengineering/platform'
import task, { TaskType, calculateStatuses, createState, findStatusAttr } from '@hcengineering/task'
import tracker, { IssueStatus } from '@hcengineering/tracker'
import task from '@hcengineering/task'
import { IssueStatus } from '@hcengineering/tracker'
import { deepEqual } from 'fast-equals'
import { IntegrationManager, githubExternalSyncVersion } from '../types'
import { GithubDataType } from './githubTypes'
import { githubExternalSyncVersion } from '../types'
/**
* Return if github write operations are allowed.
@@ -148,58 +144,6 @@ export function gqlp (params: Record<string, string | number | string[] | undefi
return result
}
/**
* @public
*/
export async function getCreateStatus (
ctx: MeasureContext,
provider: IntegrationManager,
client: TxOperations,
prj: GithubProject,
name: string,
description: string,
colorStr: string,
taskType: TaskType
): Promise<string> {
const color = hashCode(colorStr)
const states = await provider.getStatuses(taskType._id)
for (const s of states) {
if (s.name.toLowerCase().trim() === name.toLowerCase().trim()) {
return s._id
}
}
ctx.error('Create new project Status', { name, colorStr, category: 'Backlog' })
// No status found, let's create one.
const id = await createState(client, taskType.statusClass, {
name,
description,
color,
ofAttribute: findStatusAttr(client.getHierarchy(), taskType.statusClass)._id,
category: task.statusCategory.UnStarted
})
const type = await client.findOne(task.class.ProjectType, { _id: prj.type })
if (type === undefined) {
return id
}
if (!taskType.statuses.includes(id)) {
await client.update(taskType, {
$push: { statuses: id }
})
const taskTypes = toIdMap(await client.findAll(task.class.TaskType, { parent: type._id }))
const index = type.statuses.findIndex((it) => it._id === id)
if (index === -1) {
await client.update(type, {
statuses: calculateStatuses(type, taskTypes, [{ taskTypeId: taskType._id, statuses: taskType.statuses }])
})
}
}
return id
}
/**
* @public
*/
@@ -207,39 +151,6 @@ export function hashCode (str: string): number {
return str.split('').reduce((prevHash, currVal) => ((prevHash << 5) - prevHash + currVal.charCodeAt(0)) | 0, 0)
}
export function getType (attr: AnyAttribute): GithubDataType | undefined {
if (attr.type._class === core.class.TypeString) {
return 'TEXT'
}
if (
attr.type._class === core.class.TypeNumber ||
attr.type._class === tracker.class.TypeReportedTime ||
attr.type._class === tracker.class.TypeEstimation ||
attr.type._class === tracker.class.TypeRemainingTime
) {
return 'NUMBER'
}
if (attr.type._class === core.class.TypeDate) {
return 'DATE'
}
if (attr.type._class === core.class.EnumOf) {
return 'SINGLE_SELECT'
}
}
export function getPlatformType (dataType: GithubDataType): Ref<Class<Type<any>>> | undefined {
switch (dataType) {
case 'TEXT':
return core.class.TypeString
case 'NUMBER':
return core.class.TypeNumber
case 'DATE':
return core.class.TypeDate
case 'SINGLE_SELECT':
return core.class.EnumOf
}
}
export async function guessStatus (
pr: { state: 'OPEN' | 'CLOSED' | 'MERGED', stateReason?: GithubIssueStateReason | null },
statuses: Status[]
-4
View File
@@ -18,7 +18,6 @@ import {
DocSyncInfo,
GithubIntegration,
GithubIntegrationRepository,
GithubMilestone,
GithubProject,
GithubUserInfo
} from '@hcengineering/github'
@@ -27,7 +26,6 @@ import { ProjectType, TaskType } from '@hcengineering/task'
import { MarkupNode } from '@hcengineering/text'
import { User } from '@octokit/webhooks-types'
import { Octokit } from 'octokit'
import { GithubProjectV2 } from './sync/githubTypes'
/**
* @public
@@ -61,8 +59,6 @@ export interface IntegrationContainer {
installationName: string
octokit: Octokit
projectStructure: Map<Ref<GithubProject | GithubMilestone>, GithubProjectV2>
enabled: boolean
synchronized: Set<string>
+1 -8
View File
@@ -79,13 +79,11 @@ import { createNotification } from './notifications'
import { InstallationRecord, PlatformWorker } from './platform'
import { CommentSyncManager } from './sync/comments'
import { IssueSyncManager } from './sync/issues'
import { ProjectsSyncManager } from './sync/projects'
import { PullRequestSyncManager } from './sync/pullrequests'
import { RepositorySyncMapper } from './sync/repository'
import { ReviewCommentSyncManager } from './sync/reviewComments'
import { ReviewThreadSyncManager } from './sync/reviewThreads'
import { ReviewSyncManager } from './sync/reviews'
import { syncConfig } from './sync/syncConfig'
import { UsersSyncManager, fetchViewerDetails } from './sync/users'
import { errorToObj } from './sync/utils'
import {
@@ -407,10 +405,6 @@ export class GithubWorker implements IntegrationManager {
this.mappers = [
{ _class: [github.mixin.GithubProject], mapper: this.repositoryManager },
{
_class: [github.class.GithubIntegration, tracker.class.Milestone],
mapper: new ProjectsSyncManager(this.ctx.newChild('project', {}), this._client, this.liveQuery)
},
{
_class: [tracker.class.Issue],
mapper: new IssueSyncManager(this.ctx.newChild('issue', {}), this._client, this.liveQuery, this.collaborator)
@@ -861,7 +855,6 @@ export class GithubWorker implements IntegrationManager {
installationName: inst?.installationName ?? '',
enabled: !inst.suspended,
synchronized: new Set(),
projectStructure: new Map(),
syncLock: new Map()
}
this.integrations.set(it.installationId, current)
@@ -1287,7 +1280,7 @@ export class GithubWorker implements IntegrationManager {
if (it.enabled) {
const _projects = []
for (const p of allProjects) {
if (p.integration === it.integration._id && (!syncConfig.MainProject || it.projectStructure.has(p._id))) {
if (p.integration === it.integration._id) {
_projects.push(p)
}
}