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
26 changes: 26 additions & 0 deletions .github/actions/update-linear-status/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Update Linear Status

inputs:
linearApiKey:
required: true
teamName:
required: true
targetLabel:
required: true

runs:
using: 'composite'
steps:
- uses: actions/checkout@v4

- name: Install dependencies
shell: bash
run: pnpm install tsx -w --save-dev

- name: Update issues
shell: bash
run: npx tsx ./.github/scripts/update-linear.ts
env:
LINEAR_API_KEY: ${{inputs.linearApiKey}}
TEAM_NAME: ${{inputs.teamName}}
TARGET_LABEL: ${{inputs.targetLabel}}
93 changes: 93 additions & 0 deletions .github/scripts/update-linear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { LinearClient, Team, WorkflowState, IssueLabel } from '@linear/sdk'

const LINEAR_API_KEY = process.env.LINEAR_API_KEY
const SOURCE_STATE_NAME = 'Staging'
const TARGET_STATE_NAME = 'Production (Done)'
const TEAM_NAME = process.env.TEAM_NAME
const TARGET_LABEL = process.env.TARGET_LABEL

if (!LINEAR_API_KEY) {
throw new Error('No LINEAR_API_KEY environment variable')
}

if (!TEAM_NAME) {
throw new Error('No TEAM_NAME environment variable')
}

if (!TARGET_LABEL) {
throw new Error('No TARGET_LABEL environment variable')
}

void updateLinearIssues()

async function getTeam(): Promise<Team> {
const client = new LinearClient({ apiKey: LINEAR_API_KEY })
const teams = await client.teams()
const targetTeam = teams.nodes.find((t) => t.name === TEAM_NAME)

if (!targetTeam) throw new Error(`Could not find team with name "${TEAM_NAME}"`)
return targetTeam
}

async function getStates(targetTeam: Team): Promise<{ sourceState: WorkflowState; targetState: WorkflowState }> {
const states = await targetTeam.states()
const sourceState = states.nodes.find((s) => s.name === SOURCE_STATE_NAME)
const targetState = states.nodes.find((s) => s.name === TARGET_STATE_NAME)

if (!sourceState) throw new Error(`Could not find workflow state ${SOURCE_STATE_NAME}`)
if (!targetState) throw new Error(`Could not find workflow state ${TARGET_STATE_NAME}`)

console.log(`Found source state: ${sourceState.name} (${sourceState.id})`)
console.log(`Found target state: ${targetState.name} (${targetState.id})`)

return { sourceState, targetState }
}

async function getTargetLabels(targetTeam: Team): Promise<IssueLabel[]> {
let labels = await targetTeam.labels()
const labelsArray = [...labels.nodes]

while (labels.pageInfo.hasNextPage) {
labels = await labels.fetchNext()
labelsArray.push(...labels.nodes)
}

const targetLabels = labelsArray.filter(
(label) => label.name === TARGET_LABEL || label.name.startsWith(TARGET_LABEL + '/')
)

if (targetLabels.length === 0) {
throw new Error(`Could not find any labels matching ${TARGET_LABEL}`)
}

console.log(`Found ${targetLabels.length} matching label(s):`)
targetLabels.forEach((label) => {
console.log(` - ${label.name} (${label.id})`)
})

return targetLabels
}

async function updateLinearIssues() {
const targetTeam = await getTeam()
const { sourceState, targetState } = await getStates(targetTeam)
const targetLabels = await getTargetLabels(targetTeam)

const targetLabelIds = targetLabels.map((label) => label.id)

const issues = await targetTeam.issues({
filter: {
labels: { some: { id: { in: targetLabelIds } } },
state: { id: { eq: sourceState.id } },
},
})

console.log(`Found ${issues.nodes.length} issue(s) in state "${SOURCE_STATE_NAME}"`)

await Promise.all(
issues.nodes.map(async (issue) => {
console.log(`Updating issue ${issue.identifier} to ${TARGET_STATE_NAME}`)
await issue.update({ stateId: targetState.id })
})
)
}
8 changes: 8 additions & 0 deletions .github/workflows/deploy-integrations-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ jobs:
force: ${{ github.event.inputs.force == 'true' }}
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}

- name: Update Linear Status
uses: ./.github/actions/update-linear-status
continue-on-error: true
with:
linearApiKey: ${{secrets.LINEAR_API_KEY}}
teamName: 'SHELL (Integration)'
targetLabel: 'area/integration'
4 changes: 0 additions & 4 deletions bots/bugbuster/bot.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ export default new sdk.BotDefinition({
output: { schema: sdk.z.object({}) },
},
},
__advanced: {
useLegacyZuiTransformer: true,
},
})
.addIntegration(github, {
enabled: true,
Expand All @@ -55,7 +52,6 @@ export default new sdk.BotDefinition({
githubWebhookSecret: genenv.BUGBUSTER_GITHUB_WEBHOOK_SECRET,
},
})
// TODO: replace Telegram with Slack when available
.addIntegration(telegram, {
enabled: true,
configurationType: null,
Expand Down
24 changes: 24 additions & 0 deletions bots/bugbuster/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IssueProcessor } from './services/issue-processor'
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) => {
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 teamsManager = new TeamsManager(linear, client, ctx.botId)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager)

return {
botpress,
linear,
teamsManager,
issueProcessor,
}
}
40 changes: 26 additions & 14 deletions bots/bugbuster/src/handlers/github-issue-opened.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
import * as utils from '../utils'
import * as boot from '../bootstrap'
import * as bp from '.botpress'

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 = await utils.linear.LinearApi.create()
const { linear, botpress } = await boot.bootstrap(props)

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

const githubLabel = await linear
.findLabel({ name: 'github', parentName: 'origin' })
.catch(_handleError('trying to find the origin/github label in Linear'))

const githubLabel = await linear.findLabel({ name: 'github', parentName: 'origin' })
if (!githubLabel) {
props.logger.error('Label origin/github not found in engineering team')
}

const linearResponse = await linear.client.createIssue({
teamId: linear.teams.ENG.id,
stateId: linear.states.ENG.TRIAGE.id,
title: githubIssue.issue.name,
description: githubIssue.issue.body,
labelIds: githubLabel ? [githubLabel.id] : [],
})
const linearResponse = await linear.client
.createIssue({
teamId: linear.teams.ENG.id,
stateId: linear.states.ENG.TRIAGE.id,
title: githubIssue.issue.name,
description: githubIssue.issue.body,
labelIds: githubLabel ? [githubLabel.id] : [],
})
.catch(_handleError('trying to create a Linear issue from the GitHub issue'))

const comment = [
'This issue was created from GitHub by BugBuster Bot.',
'',
`GitHub Issue: [${githubIssue.issue.name}](${githubIssue.issue.url})`,
].join('\n')

await linear.client.createComment({
issueId: linearResponse.issueId,
body: comment,
})
await linear.client
.createComment({
issueId: linearResponse.issueId,
body: comment,
})
.catch(_handleError('trying to create a comment on the Linear issue created from GitHub'))
}
109 changes: 0 additions & 109 deletions bots/bugbuster/src/handlers/issue-processor.ts

This file was deleted.

19 changes: 12 additions & 7 deletions bots/bugbuster/src/handlers/linear-issue-created.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import * as utils from '../utils'
import { IssueProcessor } from './issue-processor'
import * as boot from '../bootstrap'
import * as bp from '.botpress'

export const handleLinearIssueCreated: bp.EventHandlers['linear:issueCreated'] = async (props) => {
const { client, event, logger, ctx } = props
const { event } = props
const { number: issueNumber, teamKey } = event.payload
const linear = await utils.linear.LinearApi.create()
const issueProcessor = new IssueProcessor(logger, linear, client, ctx.botId)
const issue = await issueProcessor.findIssue(issueNumber, teamKey, 'created')

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

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

props.logger.info('Linear issue created event received', `${teamKey}-${issueNumber}`)
const issue = await issueProcessor
.findIssue(issueNumber, teamKey)
.catch(_handleError('trying to find the created Linear issue'))

if (!issue) {
return
}

await issueProcessor.runLint(issue)
await issueProcessor.lintIssue(issue).catch(_handleError('trying to lint the created Linear issue'))
}
Loading
Loading