-
-
Save futurGH/00f4b9a76636e91af40a7aca9aeb3a2e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { graphql as _graphql } from "@octokit/graphql"; | |
import { Bot, Post, type ReplyRef, RichText, type StrongRef } from "@skyware/bot"; | |
import { JSONFilePreset } from "lowdb/node"; | |
import cron from "node-cron"; | |
if (!process.env.GITHUB_TOKEN || !process.env.BOT_PASSWORD) { | |
throw new Error("Missing environment variable"); | |
} | |
const db = await JSONFilePreset("db.json", { | |
repositories: [] as Array<string>, | |
prs: {} as Record<string, { title: string; url: string; postRef?: StrongRef | undefined; number: number; merged: boolean; mergedAt: string | null; createdAt: string }>, | |
branches: {} as Record<string, Array<string>> | |
}); | |
const gql = _graphql.defaults({ | |
headers: { | |
authorization: `token ${process.env.GITHUB_TOKEN}`, | |
}, | |
}); | |
const bot = new Bot({ | |
emitEvents: false, | |
}); | |
await bot.login({ identifier: "skywatch.bsky.social", password: process.env.BOT_PASSWORD }); | |
const pullRequestFragment = /* GraphQL */ ` | |
fragment PullRequest on PullRequest { | |
title | |
url | |
number | |
merged | |
mergedAt | |
createdAt | |
baseRef { | |
name | |
} | |
} | |
`; | |
const QUERY = /* GraphQL */ ` | |
${pullRequestFragment} | |
query { | |
organization(login: "bluesky-social") { | |
repositories(first: 100) { | |
nodes { | |
name | |
url | |
createdAt | |
} | |
} | |
} | |
search(first: 100, type: ISSUE, query: "org:bluesky-social is:pr sort:updated") { | |
nodes { | |
... on PullRequest { | |
...PullRequest | |
} | |
} | |
} | |
} | |
`; | |
const BRANCHES_QUERY = /* GraphQL */ ` | |
${pullRequestFragment} | |
query Branches($name: String!, $cursor: String) { | |
repository(owner: "bluesky-social", name: $name) { | |
refs(refPrefix: "refs/heads/", first: 100, after: $cursor) { | |
nodes { | |
name | |
target { | |
... on Commit { | |
authoredDate | |
} | |
} | |
associatedPullRequests(first: 1) { | |
nodes { | |
...PullRequest | |
} | |
} | |
} | |
pageInfo { | |
endCursor | |
} | |
} | |
} | |
} | |
`; | |
type PullRequest = { | |
title: string; | |
url: string; | |
number: number; | |
merged: boolean; | |
mergedAt: string | null; | |
baseRef: { name: string }; | |
createdAt: string; | |
} | |
type Branch = { name: string; target: { authoredDate: string }; associatedPullRequests: { nodes: [PullRequest]} }; | |
async function getAllBranches(name: string) { | |
const branches: Array<Branch> = []; | |
let cursor: string | null | undefined; | |
do { | |
const data = await gql< | |
{ repository: { refs: { nodes: Array<Branch>; pageInfo: { endCursor: string | null } } } } | |
>(BRANCHES_QUERY, { name, cursor }); | |
branches.push(...data.repository.refs.nodes); | |
cursor = data.repository.refs.pageInfo.endCursor; | |
} while (cursor); | |
return branches; | |
} | |
cron.schedule("*/6 * * * *", async () => { | |
const data = await gql<{ | |
organization: { repositories: { nodes: Array<{ name: string; url: string; createdAt: string }> } }; | |
search: { | |
nodes: Array<PullRequest> | |
}; | |
}>(QUERY); | |
const repositories = db.data.repositories || []; | |
if (repositories.length) { | |
for (const repo of data.organization.repositories.nodes) { | |
const isNew = !repositories.includes(repo.name) && new Date(repo.createdAt) > new Date(Date.now() - 1000 * 60 * 60 * 24); | |
if (isNew) { | |
console.log("Posted new repo: ", repo.url, repo.name, repo.createdAt); | |
await bot.post({ | |
text: new RichText().text("New bluesky-social repository created: ").link(repo.name, repo.url), | |
external: repo.url | |
}); | |
await db.update(({ repositories }) => repositories.push(repo.name)); | |
} | |
const branches = await getAllBranches(repo.name); | |
if (!db.data.branches[repo.name]?.length) { | |
await db.update(db => db.branches[repo.name] = branches.map(branch => branch.name)); | |
continue; | |
} | |
for (const branch of branches) { | |
if (branch.associatedPullRequests?.nodes?.[0]?.url) continue; | |
const isNew = !db.data.branches[repo.name].includes(branch.name) && new Date(branch.target.authoredDate) > new Date(Date.now() - 1000 * 60 * 60 * 24); | |
if (isNew) { | |
const url = repo.url + "/compare/main..." + branch.name; | |
await bot.post({ | |
text: new RichText().text("New branch created in bluesky-social/" + repo.name + ": ").link(branch.name, url), | |
external: url | |
}); | |
await db.update(db => db.branches[repo.name].push(branch.name)); | |
console.log("Posted new branch: ", branch.name, branch.target.authoredDate); | |
} | |
} | |
} | |
} else { | |
// If we don't know about any repos, just save all of them this time | |
await db.update(db => db.repositories = data.organization.repositories.nodes.map((repo) => repo.name)); | |
} | |
const fetchedPrs = data.search.nodes.sort((a, b) => | |
new Date(a.mergedAt || a.createdAt).getTime() - new Date(b.mergedAt || b.createdAt).getTime() | |
); | |
console.log(`VERBOSE: Fetched ${fetchedPrs.length} PRs`); | |
for (const pr of fetchedPrs) { | |
const searchText = pr.title + ` (#${ pr.number })`; | |
const postedPr = await bot.api.app.bsky.feed.searchPosts({ q: `"${searchText}"`, author: bot.profile.did, limit: 1 }).then(res => { | |
const postView = res.data.posts[0]; | |
return postView ? Post.fromView(postView, bot) : null; | |
}) | |
const isNew = !postedPr && pr.createdAt && new Date(pr.createdAt) > new Date(Date.now() - 1000 * 60 * 60 * 24); | |
const isRecentlyMerged = pr.merged && pr.mergedAt && new Date(pr.mergedAt) > new Date(Date.now() - 1000 * 60 * 60 * 24); | |
if (isRecentlyMerged) { | |
if (postedPr?.text.startsWith("Merged PR")) { | |
continue; | |
} | |
const repoName = pr.url.match(/bluesky-social\/([^/]+)\/pull/)?.[1]; | |
const branchName = pr.baseRef?.name; | |
let mergedText = "Merged PR"; | |
if (branchName && repoName) { | |
mergedText += " into " + branchName + " at bluesky-social/" + repoName; | |
} | |
mergedText += ": "; | |
let replyRef: ReplyRef | undefined; | |
if (postedPr && postedPr.text.startsWith("New PR:")) { | |
replyRef = { | |
parent: { | |
uri: postedPr.uri, | |
cid: postedPr.cid | |
}, | |
root: postedPr.replyRef?.root ? { | |
uri: postedPr.replyRef.root.uri, | |
cid: postedPr.replyRef.root.cid | |
} : { | |
uri: postedPr.uri, | |
cid: postedPr.cid | |
} | |
}; | |
} | |
try { | |
await bot.post({ | |
text: new RichText().text(mergedText).link(pr.title + ` (#${ pr.number })`, pr.url), | |
replyRef, | |
}); | |
} catch (e) { | |
console.error("Error posting merged PR: ", pr.url, pr.title, pr.mergedAt); | |
console.error(e); | |
break; | |
} | |
} else if (isNew) { | |
try { | |
await bot.post({ | |
text: new RichText().text("New PR: ").link(pr.title + ` (#${ pr.number })`, pr.url), | |
external: pr.url, | |
}); | |
} catch (e) { | |
console.error("Error posting new PR: ", pr.url, pr.title, pr.createdAt); | |
console.error(e); | |
break; | |
} | |
} | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment