Skip to content

Instantly share code, notes, and snippets.

@futurGH
Created November 1, 2024 12:37
Show Gist options
  • Save futurGH/00f4b9a76636e91af40a7aca9aeb3a2e to your computer and use it in GitHub Desktop.
Save futurGH/00f4b9a76636e91af40a7aca9aeb3a2e to your computer and use it in GitHub Desktop.
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