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
11 changes: 5 additions & 6 deletions bots/bugbuster/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { IssueProcessor } from './services/issue-processor'
import { RecentlyLintedManager } from './services/recently-linted-manager'
import { TeamsManager } from './services/teams-manager'
import * as types from './types'
import * as utils from './utils'

export const bootstrap = async (props: types.CommonHandlerProps, conversationId?: string) => {
export const bootstrap = (props: types.CommonHandlerProps) => {
const { client, logger, ctx } = props
const botpress = utils.botpress.BotpressApi.create(props)

const _handleError = (context: string) => (thrown: unknown) =>
botpress.handleError({ context, conversationId }, thrown)

// TODO: make this synchronous so it won't slow down bootstraping or throw
const linear = await utils.linear.LinearApi.create().catch(_handleError('trying to initialize Linear API'))
const linear = utils.linear.LinearApi.create()
const teamsManager = new TeamsManager(linear, client, ctx.botId)
const recentlyLintedManager = new RecentlyLintedManager(client, ctx.botId)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager)

return {
botpress,
linear,
teamsManager,
recentlyLintedManager,
issueProcessor,
}
}
9 changes: 6 additions & 3 deletions bots/bugbuster/src/handlers/github-issue-opened.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = a

props.logger.info('Received GitHub issue', githubIssue)

const { linear, botpress } = await boot.bootstrap(props)
const { linear, botpress } = boot.bootstrap(props)

const _handleError =
(context: string) =>
Expand All @@ -21,10 +21,13 @@ export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = a
props.logger.error('Label origin/github not found in engineering team')
}

const teams = await linear.getTeamRecords().catch(_handleError('trying to get Linear teams'))
const states = await linear.getStateRecords().catch(_handleError('trying to get Linear states'))

const linearResponse = await linear.client
.createIssue({
teamId: linear.teams.ENG.id,
stateId: linear.states.ENG.TRIAGE.id,
teamId: teams.ENG.id,
stateId: states.ENG.TRIAGE.id,
title: githubIssue.issue.name,
description: githubIssue.issue.body,
labelIds: githubLabel ? [githubLabel.id] : [],
Expand Down
2 changes: 1 addition & 1 deletion bots/bugbuster/src/handlers/linear-issue-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const handleLinearIssueCreated: bp.EventHandlers['linear:issueCreated'] =
const { event } = props
const { number: issueNumber, teamKey } = event.payload

const { botpress, issueProcessor } = await boot.bootstrap(props)
const { botpress, issueProcessor } = boot.bootstrap(props)

const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)

Expand Down
18 changes: 8 additions & 10 deletions bots/bugbuster/src/handlers/linear-issue-updated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] =
const { event, logger } = props
const { number: issueNumber, teamKey } = event.payload

const { botpress, issueProcessor } = await boot.bootstrap(props)
const { botpress, issueProcessor, recentlyLintedManager } = boot.bootstrap(props)

const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)

props.logger.info('Linear issue updated event received', `${teamKey}-${issueNumber}`)
logger.info('Linear issue updated event received', `${teamKey}-${issueNumber}`)
const issue = await issueProcessor
.findIssue(issueNumber, teamKey)
.catch(_handleError('trying to find the updated Linear issue'))
Expand All @@ -18,15 +17,14 @@ export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] =
return
}

const recentlyLinted = await botpress.getRecentlyLinted().catch(_handleError('trying to get recently linted issues'))
const recentlyLinted = await recentlyLintedManager
.getRecentlyLinted()
.catch(_handleError('trying to get recently linted issues'))

if (recentlyLinted.some(({ id: issueId }) => issue.id === issueId)) {
logger.info(`Issue ${issue.identifier} has already been linted recently, skipping...`)
return
}
const isRecentlyLinted = recentlyLinted.some(({ id: issueId }) => issue.id === issueId)

await issueProcessor.lintIssue(issue).catch(_handleError('trying to lint the updated Linear issue'))
await botpress
await issueProcessor.lintIssue(issue, isRecentlyLinted).catch(_handleError('trying to lint the updated Linear issue'))
await recentlyLintedManager
.setRecentlyLinted([...recentlyLinted, { id: issue.id, lintedAt: new Date().toISOString() }])
.catch(_handleError('trying to update recently linted issues'))
}
40 changes: 23 additions & 17 deletions bots/bugbuster/src/handlers/lint-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => {

const conversationId = conversation?.id

const { botpress, issueProcessor } = await boot.bootstrap(props, conversationId)
const { botpress, issueProcessor } = boot.bootstrap(props)

const _handleError = (context: string) => (thrown: unknown) =>
botpress.handleError({ context, conversationId }, thrown)
Expand All @@ -25,22 +25,28 @@ export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => {
})
.catch(_handleError('trying to get last linted issue ID'))

const issues = await issueProcessor
.listRelevantIssues(lastLintedId) // TODO: we should not list all issues at first, bug fetch next page and lint progressively
.catch(_handleError('trying to list all issues'))

for (const issue of issues) {
await issueProcessor.lintIssue(issue).catch(_handleError(`trying to lint issue ${issue.identifier}`))
await workflow.acknowledgeStartOfProcessing().catch(_handleError('trying to acknowledge start of processing'))
await client
.setState({
id: workflow.id,
name: 'lastLintedId',
type: 'workflow',
payload: { id: issue.id },
})
.catch(_handleError('trying to update last linted issue ID'))
}
let hasNextPage = false
let endCursor: string | undefined = lastLintedId
do {
const pagedIssues = await issueProcessor
.listRelevantIssues(endCursor)
.catch(_handleError('trying to list all issues'))

for (const issue of pagedIssues.issues) {
await issueProcessor.lintIssue(issue).catch(_handleError(`trying to lint issue ${issue.identifier}`))
await workflow.acknowledgeStartOfProcessing().catch(_handleError('trying to acknowledge start of processing'))
await client
.setState({
id: workflow.id,
name: 'lastLintedId',
type: 'workflow',
payload: { id: issue.id },
})
.catch(_handleError('trying to update last linted issue ID'))
}
hasNextPage = pagedIssues.pagination?.hasNextPage ?? false
endCursor = pagedIssues.pagination?.endCursor
} while (hasNextPage)

if (conversationId) {
await botpress.respondText(conversationId, 'linted all issues').catch(() => {})
Expand Down
2 changes: 1 addition & 1 deletion bots/bugbuster/src/handlers/message-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => {
return
}

const { botpress, teamsManager } = await boot.bootstrap(props, conversation.id)
const { botpress, teamsManager } = boot.bootstrap(props)

if (message.type !== 'text') {
await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE)
Expand Down
41 changes: 18 additions & 23 deletions bots/bugbuster/src/services/issue-processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class IssueProcessor {
}

const watchedTeams = await this._teamsManager.listWatchedTeams()
if (!this._linear.isTeam(teamKey) || !watchedTeams.includes(teamKey)) {
if (!(await this._linear.isTeam(teamKey)) || !watchedTeams.includes(teamKey)) {
this._logger.info(`Ignoring issue of team "${teamKey}"`)
return
}
Expand All @@ -37,31 +37,20 @@ export class IssueProcessor {
return issue
}

public async listRelevantIssues(endCursor?: string): Promise<lin.Issue[]> {
public async listRelevantIssues(endCursor?: string): Promise<{ issues: lin.Issue[]; pagination?: lin.Pagination }> {
const watchedTeams = await this._teamsManager.listWatchedTeams()

const issues: lin.Issue[] = []
let pagination: lin.Pagination | undefined

do {
const { issues: newIssues, pagination: newPagination } = await this._linear.listIssues(
{
teamKeys: watchedTeams,
statusesToOmit: IGNORED_STATUSES,
},
endCursor
)

issues.push(...newIssues)
pagination = newPagination
endCursor = pagination?.endCursor
} while (pagination?.hasNextPage)

return issues
return await this._linear.listIssues(
{
teamKeys: watchedTeams,
statusesToOmit: IGNORED_STATUSES,
},
endCursor
)
}

public async lintIssue(issue: lin.Issue) {
const status = this._linear.issueStatus(issue)
public async lintIssue(issue: lin.Issue, isRecentlyLinted?: boolean) {
const status = await this._linear.issueStatus(issue)
if (IGNORED_STATUSES.includes(status) || issue.labels.nodes.some((label) => label.name === LINTIGNORE_LABEL_NAME)) {
return
}
Expand All @@ -74,7 +63,13 @@ export class IssueProcessor {
return
}

this._logger.warn(`Issue ${issue.identifier} has ${errors.length} lint errors:`)
const warningMessage = `Issue ${issue.identifier} has ${errors.length} lint errors.`
if (isRecentlyLinted) {
this._logger.warn(`${warningMessage} Not commenting the issue because it has been linted recently.`)
return
}

this._logger.warn(warningMessage)

await this._linear.client.createComment({
issueId: issue.id,
Expand Down
42 changes: 42 additions & 0 deletions bots/bugbuster/src/services/recently-linted-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as bp from '.botpress'

const RECENT_THRESHOLD: number = 1000 * 60 * 10 // 10 minutes
type IssueLintEntry = bp.states.recentlyLinted.RecentlyLinted['payload']['issues'][number]

export class RecentlyLintedManager {
public constructor(
private _client: bp.Client,
private _botId: string
) {}

public async getRecentlyLinted(): Promise<bp.states.recentlyLinted.RecentlyLinted['payload']['issues']> {
const {
state: {
payload: { issues },
},
} = await this._client.getOrSetState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: { issues: [] },
})
return issues.filter(this._isRecentlyLinted)
}

public async setRecentlyLinted(issues: bp.states.recentlyLinted.RecentlyLinted['payload']['issues']): Promise<void> {
await this._client.setState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: {
issues: issues.filter(this._isRecentlyLinted),
},
})
}

private _isRecentlyLinted = (issue: IssueLintEntry): boolean => {
const lintedAt = new Date(issue.lintedAt).getTime()
const now = new Date().getTime()
return now - lintedAt < RECENT_THRESHOLD
}
}
2 changes: 1 addition & 1 deletion bots/bugbuster/src/services/teams-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class TeamsManager {
if (teamKeys.includes(key)) {
throw new Error(`The team with the key '${key}' is already being watched.`)
}
if (!this._linear.isTeam(key)) {
if (!(await this._linear.isTeam(key))) {
throw new Error(`The team with the key '${key}' does not exist.`)
}

Expand Down
54 changes: 2 additions & 52 deletions bots/bugbuster/src/utils/botpress-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ import * as sdk from '@botpress/sdk'
import * as types from '../types'
import * as bp from '.botpress'

export type BotMessage = Pick<bp.ClientInputs['createMessage'], 'type' | 'payload'>
export type GithubIssue = bp.integrations.github.actions.findTarget.output.Output['targets'][number]
export type IssueLintEntry = bp.states.recentlyLinted.RecentlyLinted['payload']['issues'][number]
type BotMessage = Pick<bp.ClientInputs['createMessage'], 'type' | 'payload'>

const RECENT_THRESHOLD: number = 1000 * 60 * 10 // 10 minutes

export type ErrorHandlerProps = {
type ErrorHandlerProps = {
context: string
conversationId?: string
}

// TODO: most of this class is not really meant to be in utils, consider moving it in services like the teams-manager class
export class BotpressApi {
private constructor(
private _client: bp.Client,
Expand Down Expand Up @@ -42,45 +37,6 @@ export class BotpressApi {
})
}

public listGithubIssues = async (): Promise<GithubIssue[]> => {
const {
output: { targets: githubIssues },
} = await this._client.callAction({
type: 'github:findTarget',
input: {
channel: 'issue',
repo: 'botpress',
query: '',
},
})
return githubIssues
}

public async getRecentlyLinted(): Promise<bp.states.recentlyLinted.RecentlyLinted['payload']['issues']> {
const {
state: {
payload: { issues },
},
} = await this._client.getOrSetState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: { issues: [] },
})
return issues.filter(this._isRecentlyLinted)
}

public async setRecentlyLinted(issues: bp.states.recentlyLinted.RecentlyLinted['payload']['issues']): Promise<void> {
await this._client.setState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: {
issues: issues.filter(this._isRecentlyLinted),
},
})
}

public handleError = async (props: ErrorHandlerProps, thrown: unknown): Promise<never> => {
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
const message = `An error occured while ${props.context}: ${error.message}`
Expand All @@ -90,10 +46,4 @@ export class BotpressApi {
}
throw new sdk.RuntimeError(error.message)
}

private _isRecentlyLinted = (issue: IssueLintEntry): boolean => {
const lintedAt = new Date(issue.lintedAt).getTime()
const now = new Date().getTime()
return now - lintedAt < RECENT_THRESHOLD
}
}
Loading
Loading