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
3 changes: 1 addition & 2 deletions bots/bugbuster/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
"private": true,
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/sdk": "workspace:*",
"@linear/sdk": "^50.0.0"
"@botpress/sdk": "workspace:*"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
Expand Down
6 changes: 3 additions & 3 deletions bots/bugbuster/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export const bootstrap = (props: types.CommonHandlerProps) => {
const { client, logger, ctx } = props
const botpress = utils.botpress.BotpressApi.create(props)

const linear = utils.linear.LinearApi.create()
const linear = utils.linear.LinearApi.create(client)
const teamsManager = new TeamsManager(linear, client, ctx.botId)
const recentlyLintedManager = new RecentlyLintedManager(linear)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager)
const issueStateChecker = new IssueStateChecker(linear, logger)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager, ctx.botId)
const issueStateChecker = new IssueStateChecker(linear, logger, ctx.botId)
const commandProcessor = new CommandProcessor(client, teamsManager, ctx.botId)

return {
Expand Down
41 changes: 13 additions & 28 deletions bots/bugbuster/src/handlers/github-issue-opened.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,28 @@
import * as boot from '../bootstrap'
import * as bp from '.botpress'

const STATE_NAME_FOR_NEW_ISSUES = 'Triage'
const TEAM_KEY_FOR_NEW_ISSUES = 'ENG'
const TEAM_NAME_FOR_NEW_ISSUES = 'Engineering'

export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = async (props): Promise<void> => {
const githubIssue = props.event.payload

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

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

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

const teamStates = await linear
.findTeamStates(TEAM_KEY_FOR_NEW_ISSUES)
.catch(_handleError('trying to get Linear team states'))

if (!teamStates) {
props.logger.error(`Error: Linear team '${TEAM_KEY_FOR_NEW_ISSUES}' not found.`)
return
}

const state = teamStates.states.nodes.find((el) => el.name === STATE_NAME_FOR_NEW_ISSUES)

if (!state) {
props.logger.error(`Error: Linear state '${STATE_NAME_FOR_NEW_ISSUES}' not found.`)
return
}

const linearResponse = await linear.client
.createIssue({
teamId: teamStates.id,
stateId: state.id,
title: githubIssue.issue.name,
description: githubIssue.issue.body,
labelIds: [],
const linearResponse = await props.client
.callAction({
type: 'linear:createIssue',
input: {
teamName: TEAM_NAME_FOR_NEW_ISSUES,
description: githubIssue.issue.body,
title: githubIssue.issue.name,
},
})
.catch(_handleError('trying to create a Linear issue from the GitHub issue'))

Expand All @@ -48,10 +32,11 @@ export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = a
`GitHub Issue: [${githubIssue.issue.name}](${githubIssue.issue.url})`,
].join('\n')

await linear.client
await linear
.createComment({
issueId: linearResponse.issueId,
body: comment,
issueId: linearResponse.output.issue.id,
botId: props.ctx.botId,
})
.catch(_handleError('trying to create a comment on the Linear issue created from GitHub'))
}
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 @@ -67,7 +67,7 @@ const _buildListCommandsMessage = (definitions: CommandDefinition[]) => {
}

const _buildCommandMessage = (definition: CommandDefinition) => {
const requiredArgs = definition.requiredArgs?.map((arg) => `<${arg}>`).join(' ')
const requiredArgs = definition.requiredArgs?.map((arg) => `&lt;${arg}&gt;`).join(' ')
const optionalArgs = definition.optionalArgs?.map((arg) => `[${arg}]`).join(' ')

return `${definition.name} ${requiredArgs ?? ''} ${optionalArgs ?? ''}`
Expand Down
6 changes: 4 additions & 2 deletions bots/bugbuster/src/services/issue-processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export class IssueProcessor {
public constructor(
private _logger: sdk.BotLogger,
private _linear: lin.LinearApi,
private _teamsManager: tm.TeamsManager
private _teamsManager: tm.TeamsManager,
private _botId: string
) {}

/**
Expand Down Expand Up @@ -75,8 +76,9 @@ export class IssueProcessor {

this._logger.warn(warningMessage)

await this._linear.client.createComment({
await this._linear.createComment({
issueId: issue.id,
botId: this._botId,
body: [
`BugBuster Bot found the following problems with ${issue.identifier}:`,
'',
Expand Down
6 changes: 4 additions & 2 deletions bots/bugbuster/src/services/issue-state-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import * as lin from '../utils/linear-utils'
export class IssueStateChecker {
public constructor(
private _linear: lin.LinearApi,
private _logger: sdk.BotLogger
private _logger: sdk.BotLogger,
private _botId: string
) {}

public async processIssues(props: { stateAttributes: types.StateAttributes; teams: string[] }) {
Expand All @@ -24,8 +25,9 @@ export class IssueStateChecker {
)

for (const issue of issues) {
await this._linear.client.createComment({
await this._linear.createComment({
issueId: issue.id,
botId: this._botId,
body: stateAttributes.warningComment,
})
this._logger.warn(stateAttributes.buildWarningReason(issue.identifier))
Expand Down
4 changes: 2 additions & 2 deletions bots/bugbuster/src/services/recently-linted-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export class RecentlyLintedManager {
public constructor(private _linear: lin.LinearApi) {}

public async isRecentlyLinted(issue: lin.Issue): Promise<boolean> {
const me = await this._linear.getMe()
const me = await this._linear.getViewerId()
const timestamps = issue.comments.nodes
.filter((comment) => comment.user?.id === me.id)
.filter((comment) => comment.user?.id === me)
.map((comment) => new Date(comment.createdAt).getTime())
const now = new Date().getTime()
for (const timestamp of timestamps) {
Expand Down
13 changes: 13 additions & 0 deletions bots/bugbuster/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export type StateAttributes = {
buildWarningReason: (issueIdentifier: string) => string
}

export type LinearTeam = {
id: string
key: string
name: string
description?: string | undefined
icon?: string | undefined
}

export type LinearState = {
id: string
name: string
}

export type ISO8601Duration = string

type CommandResult = { success: boolean; message: string }
Expand Down
133 changes: 73 additions & 60 deletions bots/bugbuster/src/utils/linear-utils/client.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
import * as lin from '@linear/sdk'
import * as utils from '..'
import * as genenv from '../../../.genenv'
import * as types from '../../types'
import * as graphql from './graphql-queries'
import { Client } from '.botpress'

type State = { state: lin.WorkflowState; key: types.StateKey }
type State = { state: types.LinearState; key: types.StateKey }

const RESULTS_PER_PAGE = 200

export class LinearApi {
private _teams?: lin.Team[] = undefined
private _teams?: types.LinearTeam[] = undefined
private _states?: State[] = undefined
private _viewer?: lin.User = undefined
private _viewerId?: string = undefined

private constructor(private _client: lin.LinearClient) {}
private constructor(private _bpClient: Client) {}

public static create(): LinearApi {
const client = new lin.LinearClient({ apiKey: genenv.BUGBUSTER_LINEAR_API_KEY })

return new LinearApi(client)
}

public get client(): lin.LinearClient {
return this._client
public static create(bpClient: Client): LinearApi {
return new LinearApi(bpClient)
}

public async getMe(): Promise<lin.User> {
if (this._viewer) {
return this._viewer
public async getViewerId(): Promise<string> {
if (this._viewerId) {
return this._viewerId
}
const me = await this._client.viewer
const { output: me } = await this._bpClient.callAction({
type: 'linear:getUser',
input: {},
})
if (!me) {
throw new Error('Viewer not found. Please ensure you are authenticated.')
}
this._viewer = me
return this._viewer
this._viewerId = me.linearId
return this._viewerId
}

public async isTeam(teamKey: string) {
Expand Down Expand Up @@ -95,19 +91,6 @@ export class LinearApi {
return { issues: data.issues.nodes, pagination: data.pageInfo }
}

public async findLabel(filter: { name: string; parentName?: string }): Promise<lin.IssueLabel | undefined> {
const { name, parentName } = filter
const { nodes: labels } = await this._client.issueLabels({
filter: {
name: { eq: name },
parent: parentName ? { name: { eq: parentName } } : undefined,
},
})

const [label] = labels
return label || undefined
}

public async issueState(issue: graphql.Issue): Promise<types.StateKey> {
const states = await this.getStates()
const state = states.find((s) => s.state.id === issue.state.id)
Expand All @@ -119,17 +102,35 @@ export class LinearApi {

public async resolveComments(issue: graphql.Issue): Promise<void> {
const comments = issue.comments.nodes
const me = await this.getMe()
const me = await this.getViewerId()

const promises: Promise<lin.CommentPayload>[] = []
const promises: ReturnType<typeof this._bpClient.callAction<'linear:resolveComment'>>[] = []
for (const comment of comments) {
if (comment.user?.id === me.id && !comment.parentId && !comment.resolvedAt) {
promises.push(this._client.commentResolve(comment.id))
if (comment.user?.id === me && !comment.parentId && !comment.resolvedAt) {
promises.push(this._bpClient.callAction({ type: 'linear:resolveComment', input: { id: comment.id } }))
}
}
await Promise.all(promises)
}

public async createComment(props: { body: string; issueId: string; botId: string }): Promise<void> {
const { body, issueId, botId } = props
const conversation = await this._bpClient.callAction({
type: 'linear:getOrCreateIssueConversation',
input: {
conversation: { id: issueId },
},
})

await this._bpClient.createMessage({
type: 'text',
conversationId: conversation.output.conversationId,
payload: { text: body },
tags: {},
userId: botId,
})
}

public async findTeamStates(teamKey: string): Promise<graphql.TeamStates | undefined> {
const queryInput: graphql.GRAPHQL_QUERIES['findTeamStates'][graphql.QUERY_INPUT] = {
filter: { key: { eq: teamKey } },
Expand All @@ -144,7 +145,7 @@ export class LinearApi {
return team
}

public async getTeams(): Promise<lin.Team[]> {
public async getTeams(): Promise<types.LinearTeam[]> {
if (!this._teams) {
this._teams = await this._listAllTeams()
}
Expand All @@ -170,29 +171,31 @@ export class LinearApi {
})
}

private _listAllTeams = async (): Promise<lin.Team[]> => {
let teams: lin.Team[] = []
let cursor: string | undefined = undefined
do {
const response = await this._client.teams({ after: cursor, first: 100 })
teams = teams.concat(response.nodes)
cursor = response.pageInfo.endCursor
} while (cursor)
return teams
}

private _listAllStates = async (): Promise<lin.WorkflowState[]> => {
let states: lin.WorkflowState[] = []
let cursor: string | undefined = undefined
do {
const response = await this._client.workflowStates({ after: cursor, first: 100 })
states = states.concat(response.nodes)
cursor = response.pageInfo.endCursor
} while (cursor)
private _listAllTeams = async (): Promise<types.LinearTeam[]> => {
const response = await this._bpClient.callAction({ type: 'linear:listTeams', input: {} })
return response.output.teams
}

private _listAllStates = async (): Promise<types.LinearState[]> => {
let response = await this._bpClient.callAction<'linear:listStates'>({
type: 'linear:listStates',
input: { count: 100 },
})
let states: types.LinearState[] = response.output.states
let startCursor = response.output.nextCursor

while (startCursor) {
response = await this._bpClient.callAction<'linear:listStates'>({
type: 'linear:listStates',
input: { count: 100, startCursor },
})
states = states.concat(response.output.states)
startCursor = response.output.nextCursor
}
return states
}

private static _toStateObjects(states: lin.WorkflowState[]): State[] {
private static _toStateObjects(states: types.LinearState[]): State[] {
const stateObjects: State[] = []
for (const state of states) {
const key = utils.string.toScreamingSnakeCase(state.name) as types.StateKey
Expand All @@ -205,7 +208,17 @@ export class LinearApi {
queryName: K,
variables: graphql.GRAPHQL_QUERIES[K][graphql.QUERY_INPUT]
): Promise<graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE]> {
return (await this._client.client.rawRequest(graphql.GRAPHQL_QUERIES[queryName].query, variables))
.data as graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE]
const params = Object.entries(variables).map(([name, value]) => ({
name,
value,
}))
const result = await this._bpClient.callAction({
type: 'linear:sendRawGraphqlQuery',
input: {
query: graphql.GRAPHQL_QUERIES[queryName].query,
parameters: params,
},
})
return result.output.result as graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE]
}
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@apidevtools/json-schema-ref-parser": "^11.7.0",
"@botpress/chat": "0.5.4",
"@botpress/client": "1.29.0",
"@botpress/sdk": "5.2.0",
"@botpress/sdk": "5.3.0",
"@bpinternal/const": "^0.1.0",
"@bpinternal/tunnel": "^0.1.1",
"@bpinternal/verel": "^0.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/templates/empty-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"private": true,
"dependencies": {
"@botpress/client": "1.29.0",
"@botpress/sdk": "5.2.0"
"@botpress/sdk": "5.3.0"
},
"devDependencies": {
"@types/node": "^22.16.4",
Expand Down
Loading
Loading