Files
botpress--botpress/integrations/github/src/events/push/push-received.ts
T
2026-07-13 13:34:48 +08:00

137 lines
4.1 KiB
TypeScript

import type { PushEvent } from '@octokit/webhooks-types'
import * as mapping from '../../files-readonly/mapping'
import * as bp from '.botpress'
const MAX_BATCH_SIZE = 50
export const firePushReceived = async ({
githubEvent,
client,
logger,
}: bp.HandlerProps & { githubEvent: PushEvent }) => {
try {
const repo = githubEvent.repository
const owner = repo.owner.login ?? repo.owner.name
const repoName = repo.name
const defaultRef = `refs/heads/${repo.default_branch ?? repo.master_branch ?? 'main'}`
if (githubEvent.ref !== defaultRef) {
logger.forBot().debug(`Ignoring push to non-default branch: ${githubEvent.ref}`)
return
}
const fileStates = new Map<string, 'created' | 'updated' | 'deleted'>()
for (const commit of githubEvent.commits) {
for (const filePath of commit.added ?? []) {
const prev = fileStates.get(filePath)
fileStates.set(filePath, prev === 'deleted' ? 'updated' : 'created')
}
for (const filePath of commit.modified ?? []) {
const prev = fileStates.get(filePath)
if (prev !== 'created') {
fileStates.set(filePath, 'updated')
}
}
for (const filePath of commit.removed ?? []) {
const prev = fileStates.get(filePath)
if (prev === 'created') {
fileStates.delete(filePath)
} else {
fileStates.set(filePath, 'deleted')
}
}
}
if (fileStates.size === 0) {
return
}
const created: bp.events.Events['aggregateFileChanges']['modifiedItems']['created'] = []
const updated: bp.events.Events['aggregateFileChanges']['modifiedItems']['updated'] = []
const deleted: bp.events.Events['aggregateFileChanges']['modifiedItems']['deleted'] = []
for (const [filePath, state] of fileStates) {
const file = mapping.mapPushFileToFile(owner, repoName, filePath)
if (state === 'created') {
created.push(file)
} else if (state === 'updated') {
updated.push(file)
} else {
deleted.push(file)
}
}
await _emitFileChangeEvents({ client, logger, changes: { created, updated, deleted } })
} catch (err: unknown) {
const error = err instanceof Error ? err : new Error(String(err))
logger.forBot().error('Failed to process push event; swallowing to prevent webhook retries', error)
}
}
const _emitFileChangeEvents = async ({
client,
logger,
changes,
}: {
client: bp.Client
logger: bp.Logger
changes: FileChanges
}) => {
try {
for (const batch of _getBatches(changes)) {
await client.createEvent({
type: 'aggregateFileChanges',
payload: {
modifiedItems: batch,
},
})
}
} catch (err: unknown) {
const error = err instanceof Error ? err : new Error(String(err))
logger.forBot().error('Failed to emit file-change events; swallowing to prevent webhook retries', error)
}
}
type FileChanges = {
created: bp.events.Events['aggregateFileChanges']['modifiedItems']['created']
updated: bp.events.Events['aggregateFileChanges']['modifiedItems']['updated']
deleted: bp.events.Events['aggregateFileChanges']['modifiedItems']['deleted']
}
function* _getBatches(changes: FileChanges): Generator<FileChanges> {
let createdIdx = 0
let updatedIdx = 0
let deletedIdx = 0
while (
createdIdx < changes.created.length ||
updatedIdx < changes.updated.length ||
deletedIdx < changes.deleted.length
) {
const batch: FileChanges = { created: [], updated: [], deleted: [] }
let size = 0
while (size < MAX_BATCH_SIZE) {
const startSize = size
if (createdIdx < changes.created.length && size < MAX_BATCH_SIZE) {
batch.created.push(changes.created[createdIdx++]!)
size++
}
if (updatedIdx < changes.updated.length && size < MAX_BATCH_SIZE) {
batch.updated.push(changes.updated[updatedIdx++]!)
size++
}
if (deletedIdx < changes.deleted.length && size < MAX_BATCH_SIZE) {
batch.deleted.push(changes.deleted[deletedIdx++]!)
size++
}
if (size === startSize) {
break
}
}
yield batch
}
}