comfy-pr 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +225 -0
- package/next-env.d.ts +5 -0
- package/package.json +115 -0
- package/src/CMNodes.ts +60 -0
- package/src/CNRepos.ts +72 -0
- package/src/CRNodes.ts +26 -0
- package/src/CRPulls.ts +4 -0
- package/src/FORK_OWNER.ts +5 -0
- package/src/FORK_PREFIX.ts +5 -0
- package/src/FollowRules.ts +25 -0
- package/src/GIT_USEREMAIL.ts +5 -0
- package/src/GIT_USERNAME.ts +9 -0
- package/src/GithubIssueComments.ts +3 -0
- package/src/PushedBranch.ts +3 -0
- package/src/Totals.ts +9 -0
- package/src/TrackingPRs.ts +12 -0
- package/src/WorkerInstances.ts +89 -0
- package/src/analyzePullsStatus.ts +115 -0
- package/src/analyzeTotals.test.ts +5 -0
- package/src/analyzeTotals.ts +114 -0
- package/src/checkComfyActivated.ts +39 -0
- package/src/cli.test.ts +0 -0
- package/src/cli.ts +40 -0
- package/src/clone_modify_push_Branches.ts +21 -0
- package/src/createComfyRegistryPRsFromCandidates.ts +55 -0
- package/src/createComfyRegistryPullRequests.ts +22 -0
- package/src/createGithubForkForRepo.ts +37 -0
- package/src/createGithubPullRequest.ts +94 -0
- package/src/createIssueComment.ts +29 -0
- package/src/fetchCMNodes.ts +22 -0
- package/src/fetchComfyRegistryNodes.ts +11 -0
- package/src/fetchCurrentGeoInfo.ts +6 -0
- package/src/fetchRelatedPullWithComments.ts +15 -0
- package/src/fetchRepoDescriptionMap.ts +15 -0
- package/src/followRuleSchema.ts +69 -0
- package/src/getActivatedShell.ts +18 -0
- package/src/getBranchWorkingDir.ts +16 -0
- package/src/getRepoWorkingDir.ts +6 -0
- package/src/ghUser.ts +6 -0
- package/src/index.ts +22 -0
- package/src/initializeFollowRules.ts +26 -0
- package/src/makePublishBranch.ts +58 -0
- package/src/makeTomlBranch.ts +53 -0
- package/src/matchRelatedPulls.ts +25 -0
- package/src/muteInstances.ts +16 -0
- package/src/parseIssueUrl.ts +6 -0
- package/src/parseOwnerRepo.ts +33 -0
- package/src/parsePullUrl.ts +6 -0
- package/src/parsePullsState.ts +6 -0
- package/src/parseTitleBodyOfMarkdown.ts +8 -0
- package/src/postSlackMessage.ts +21 -0
- package/src/preload.ts +30 -0
- package/src/pullStatusFollowSchema.ts +12 -0
- package/src/readTemplateTitle.ts +11 -0
- package/src/summaryLastPullComment.ts +8 -0
- package/src/tomlFillDescription.ts +17 -0
- package/src/updateCMNodesDuplicationWarnings.ts +56 -0
- package/src/updateCMRepos.ts +27 -0
- package/src/updateCNRepos.ts +58 -0
- package/src/updateCNReposCRPullsComments.ts +41 -0
- package/src/updateCNReposInfo.ts +57 -0
- package/src/updateCNReposPRCandidate.ts +85 -0
- package/src/updateCNReposPulls.ts +28 -0
- package/src/updateCNReposPullsDashboard.ts +49 -0
- package/src/updateCNReposRelatedPulls.ts +25 -0
- package/src/updateCRNodes.ts +50 -0
- package/src/updateCRRepos.ts +26 -0
- package/src/updateComfyTotals.ts +40 -0
- package/src/updateFollowRuleSet.ts +153 -0
- package/src/updateOutdatedPullsTemplates.ts +137 -0
- package/src/updateSlackMessages.ts +43 -0
- package/tailwind.config.ts +20 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getMAC } from "@ctrl/mac-address";
|
|
2
|
+
import { defer } from "lodash-es";
|
|
3
|
+
import md5 from "md5";
|
|
4
|
+
import type { WithId } from "mongodb";
|
|
5
|
+
import { db } from "./db";
|
|
6
|
+
import { fetchCurrentGeoInfo } from "./fetchCurrentGeoInfo";
|
|
7
|
+
import { createInstanceId } from "./utils/createInstanceId";
|
|
8
|
+
export type GeoInfo = Awaited<ReturnType<typeof fetchCurrentGeoInfo>>;
|
|
9
|
+
export type WorkerInstance = {
|
|
10
|
+
/** id: rand */
|
|
11
|
+
id: string;
|
|
12
|
+
up: Date;
|
|
13
|
+
active: Date;
|
|
14
|
+
geo: GeoInfo;
|
|
15
|
+
workerId: string;
|
|
16
|
+
task?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const k = "COMFY_PR_WorkerInstanceKey";
|
|
20
|
+
type g = typeof globalThis & { [k]: any };
|
|
21
|
+
function getWorkerInstanceId() {
|
|
22
|
+
// ensure only one instance
|
|
23
|
+
if (!(global as any as g)[k])
|
|
24
|
+
defer(async function () {
|
|
25
|
+
await Promise.all([postWorkerHeartBeatLoop(), watchWorkerInstancesLoop()]);
|
|
26
|
+
});
|
|
27
|
+
const instanceId = ((global as any as g)[k] ??= createInstanceId());
|
|
28
|
+
return instanceId;
|
|
29
|
+
}
|
|
30
|
+
export const WorkerInstances = db.collection<WorkerInstance>("WorkerInstances");
|
|
31
|
+
await WorkerInstances.createIndex({ id: 1 }, { unique: true });
|
|
32
|
+
await WorkerInstances.createIndex({ ip: 1 });
|
|
33
|
+
export const _geoPromise = fetchCurrentGeoInfo(); // in background
|
|
34
|
+
|
|
35
|
+
if (import.meta.main) {
|
|
36
|
+
console.log(await getWorkerInstance());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function postWorkerHeartBeatLoop() {
|
|
40
|
+
// 30s heartbeat
|
|
41
|
+
while (true) {
|
|
42
|
+
await new Promise((r) => setTimeout(r, 30e3));
|
|
43
|
+
await getWorkerInstance();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function watchWorkerInstancesLoop() {
|
|
48
|
+
const me = await getWorkerInstance();
|
|
49
|
+
console.log("[INIT] Worker instance " + me.id + " is up.");
|
|
50
|
+
for await (const event of WorkerInstances.watch([], {
|
|
51
|
+
fullDocument: "whenAvailable",
|
|
52
|
+
})) {
|
|
53
|
+
const { fullDocument: updated } = event as typeof event & {
|
|
54
|
+
fullDocument?: WithId<WorkerInstance>;
|
|
55
|
+
};
|
|
56
|
+
if (updated && updated.id !== me.id) {
|
|
57
|
+
console.log("Another worker is updated", updated);
|
|
58
|
+
if (+updated.up > +me.up && updated.task === me.task) {
|
|
59
|
+
console.log("[EXIT] I'm outdated, new instance is: " + updated.id);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getWorkerInstance(task?: string) {
|
|
67
|
+
const id = getWorkerInstanceId();
|
|
68
|
+
if (task) {
|
|
69
|
+
console.log("Working on task: ", task);
|
|
70
|
+
}
|
|
71
|
+
return (await WorkerInstances.findOneAndUpdate(
|
|
72
|
+
{ id },
|
|
73
|
+
{
|
|
74
|
+
$set: {
|
|
75
|
+
id,
|
|
76
|
+
active: new Date(),
|
|
77
|
+
workerId: getWorkerId(),
|
|
78
|
+
geo: await _geoPromise,
|
|
79
|
+
...(task && { task }),
|
|
80
|
+
},
|
|
81
|
+
$setOnInsert: { up: new Date() },
|
|
82
|
+
},
|
|
83
|
+
{ upsert: true, returnDocument: "after" },
|
|
84
|
+
))!;
|
|
85
|
+
}
|
|
86
|
+
function getWorkerId() {
|
|
87
|
+
const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME;
|
|
88
|
+
return md5(`SALT=v9yJQouMC22do66t ${hostname} ${getMAC()}`).slice(0, 8);
|
|
89
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
|
|
2
|
+
import { peekYaml } from "peek-log";
|
|
3
|
+
import prettyMs from "pretty-ms";
|
|
4
|
+
import type { z } from "zod";
|
|
5
|
+
import { CNRepos, type CRPull } from "./CNRepos";
|
|
6
|
+
import type { GithubIssueComment } from "./GithubIssueComments";
|
|
7
|
+
import { db } from "./db";
|
|
8
|
+
import type { Task } from "./utils/Task";
|
|
9
|
+
import type { zPullStatus } from "./zod/zPullsStatus";
|
|
10
|
+
// import { $pipeline } from "./db/$pipeline";
|
|
11
|
+
// in case of dump production in local environment:
|
|
12
|
+
// bun --env-file .env.production.local src/dump.ts > dump.csv
|
|
13
|
+
export const DashboardDetails = db.collection<any>("DashboardDetails");
|
|
14
|
+
if (import.meta.main) {
|
|
15
|
+
const r = peekYaml(await analyzePullsStatus());
|
|
16
|
+
// await mkdir(".cache").catch(() => null);
|
|
17
|
+
// await writeFile(".cache/dump.yaml", YAML.stringify(r));
|
|
18
|
+
// await writeFile(".cache/dump.csv", csvFormat(r));
|
|
19
|
+
// console.log("done");
|
|
20
|
+
// generate zod schema
|
|
21
|
+
// await writeFile("src/zPullsStatus.ts", jsonToZod(await analyzePullsStatus({ limit: 1 }), "zPullsStatus", true));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PullStatus = z.infer<typeof zPullStatus>;
|
|
25
|
+
export type PullsStatus = PullStatus[] &
|
|
26
|
+
{
|
|
27
|
+
lastwords: string;
|
|
28
|
+
repository: string;
|
|
29
|
+
author_email: string;
|
|
30
|
+
ownername: string;
|
|
31
|
+
on_registry: boolean;
|
|
32
|
+
state: "OPEN" | "MERGED" | "CLOSED";
|
|
33
|
+
url: string;
|
|
34
|
+
head: string;
|
|
35
|
+
comments: number;
|
|
36
|
+
updated: string;
|
|
37
|
+
}[];
|
|
38
|
+
export async function analyzePullsStatus({ skip = 0, limit = 0 } = {}) {
|
|
39
|
+
"use server";
|
|
40
|
+
return await analyzePullsStatusPipeline()
|
|
41
|
+
.skip(skip)
|
|
42
|
+
.limit(limit || 2 ** 31 - 1)
|
|
43
|
+
.aggregate()
|
|
44
|
+
.map(({ updated_at, created_at, on_registry_at, ...pull }) => {
|
|
45
|
+
const updated = prettyMs(+new Date() - +new Date(updated_at), { compact: true }) + " ago";
|
|
46
|
+
return {
|
|
47
|
+
updated, //: updated === created ? "never" : updated,
|
|
48
|
+
...pull,
|
|
49
|
+
lastwords: pull.lastwords?.replace(/\s+/g, " ").replace(/\*\*\*.*/g, "..."),
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.toArray();
|
|
53
|
+
}
|
|
54
|
+
export function analyzePullsStatusPipeline() {
|
|
55
|
+
return (
|
|
56
|
+
$pipeline(CNRepos)
|
|
57
|
+
.unwind("$crPulls.data")
|
|
58
|
+
.match({ "crPulls.data.comments.data": { $exists: true } })
|
|
59
|
+
.set({ "crPulls.data.pull.repo": "$repository" })
|
|
60
|
+
.set({ "crPulls.data.pull.on_registry": "$on_registry" })
|
|
61
|
+
.set({ "crPulls.data.pull.type": "$crPulls.data.type" })
|
|
62
|
+
.set({ "crPulls.data.pull.comments": "$crPulls.data.comments.data" })
|
|
63
|
+
.set({ "crPulls.data.pull": "$crPulls.data.pull" })
|
|
64
|
+
.replaceRoot({ newRoot: "$crPulls.data.pull" })
|
|
65
|
+
.as<
|
|
66
|
+
CRPull & {
|
|
67
|
+
repo: string;
|
|
68
|
+
on_registry: Task<boolean>;
|
|
69
|
+
type: string;
|
|
70
|
+
comments: GithubIssueComment[];
|
|
71
|
+
}
|
|
72
|
+
>()
|
|
73
|
+
.project({
|
|
74
|
+
created_at: { $toDate: "$created_at" },
|
|
75
|
+
updated_at: { $toDate: "$updated_at" },
|
|
76
|
+
repository: 1,
|
|
77
|
+
on_registry: "$on_registry.data",
|
|
78
|
+
on_registry_at: "$on_registry.mtime",
|
|
79
|
+
state: { $toUpper: `$prState` },
|
|
80
|
+
url: "$html_url",
|
|
81
|
+
author_email: "$base.user.email",
|
|
82
|
+
ownername: "$base.user.login",
|
|
83
|
+
head: { $concat: ["$user.login", ":", "$type"] },
|
|
84
|
+
comments: { $size: "$comments" },
|
|
85
|
+
lastwords: { $arrayElemAt: ["$comments", -1] },
|
|
86
|
+
})
|
|
87
|
+
.set({ lastwords: { $concat: ["$lastwords.user.login", ": ", "$lastwords.body"] } })
|
|
88
|
+
.set({ lastwords: { $ifNull: ["$lastwords", ""] } })
|
|
89
|
+
// .set({ state: { $nin: ["CLOSED"] } })
|
|
90
|
+
.set({
|
|
91
|
+
CLOSED: { $eq: ["$state", "CLOSED"] },
|
|
92
|
+
MERGED: { $eq: ["$state", "MERGED"] },
|
|
93
|
+
OPEN: { $eq: ["$state", "OPEN"] },
|
|
94
|
+
})
|
|
95
|
+
.sort({ ownername: 1 })
|
|
96
|
+
.sort({ OPEN: -1, MERGED: -1, CLOSED: -1, updated_at: 1 })
|
|
97
|
+
.unset(["CLOSED", "MERGED", "OPEN"])
|
|
98
|
+
.as<{
|
|
99
|
+
updated_at: Date;
|
|
100
|
+
created_at: Date;
|
|
101
|
+
repository: string;
|
|
102
|
+
author_email: string;
|
|
103
|
+
ownername: string;
|
|
104
|
+
on_registry: boolean;
|
|
105
|
+
on_registry_at: Date;
|
|
106
|
+
state: "OPEN" | "MERGED" | "CLOSED";
|
|
107
|
+
url: string;
|
|
108
|
+
head: string;
|
|
109
|
+
comments: number;
|
|
110
|
+
lastwords: string;
|
|
111
|
+
}>()
|
|
112
|
+
// .stage({ ...(!!skip && { $skip: skip }) })
|
|
113
|
+
// .stage({ ...(!!limit && { $limit: limit }) })
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
|
|
3
|
+
import promiseAllProperties from "promise-all-properties";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { CMNodes } from "./CMNodes";
|
|
6
|
+
import { CNRepos } from "./CNRepos";
|
|
7
|
+
import { CRNodes } from "./CRNodes";
|
|
8
|
+
import { $filaten } from "./db";
|
|
9
|
+
import { tLog } from "./utils/tLog";
|
|
10
|
+
|
|
11
|
+
if (import.meta.main) {
|
|
12
|
+
await tLog("analyzeTotals", async () => {
|
|
13
|
+
console.log(YAML.stringify(await analyzeTotals()));
|
|
14
|
+
return [];
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @warning this function is heavy
|
|
19
|
+
*/
|
|
20
|
+
export async function analyzeTotals() {
|
|
21
|
+
"use server";
|
|
22
|
+
const totals = await promiseAllProperties({
|
|
23
|
+
Now: new Date().toISOString(),
|
|
24
|
+
"Total Nodes": promiseAllProperties({
|
|
25
|
+
"on ComfyUI Manager": CMNodes.estimatedDocumentCount(),
|
|
26
|
+
"on Registry": CRNodes.estimatedDocumentCount(),
|
|
27
|
+
}),
|
|
28
|
+
"Total Repos": $pipeline(CNRepos)
|
|
29
|
+
.group({
|
|
30
|
+
_id: null,
|
|
31
|
+
"on Comfy Manager List": { $sum: { $cond: [{ $eq: [{ $type: "$cm" }, "missing"] }, 0, 1] } },
|
|
32
|
+
"on Registry": { $sum: { $cond: [{ $eq: [{ $type: "$cr" }, "missing"] }, 0, 1] } },
|
|
33
|
+
Archived: { $sum: { $cond: ["$info.data.archived", 1, 0] } },
|
|
34
|
+
All: { $sum: 1 },
|
|
35
|
+
// Candidates: { $sum: { $cond: ["$candidate.data", 1, 0] } },
|
|
36
|
+
"Got ERROR on creating PR": { $sum: { $cond: [{ $eq: ["$createdPulls.state", "error"] }, 1, 0] } },
|
|
37
|
+
})
|
|
38
|
+
.project({ _id: 0 })
|
|
39
|
+
.aggregate()
|
|
40
|
+
// .map((e: any) => e.pairs)
|
|
41
|
+
.next(),
|
|
42
|
+
"Total Authors": $pipeline(CNRepos)
|
|
43
|
+
.match($filaten({ info: { data: { owner: { login: { $exists: true } } } } }))
|
|
44
|
+
.group({
|
|
45
|
+
_id: "$info.data.owner.login",
|
|
46
|
+
// author: "$info.data.owner.email",
|
|
47
|
+
"on Comfy Manager List": { $sum: { $cond: [{ $eq: [{ $type: "$cm" }, "missing"] }, 0, 1] } },
|
|
48
|
+
"on Registry": { $sum: { $cond: [{ $eq: [{ $type: "$cr" }, "missing"] }, 0, 1] } },
|
|
49
|
+
Archived: { $sum: { $cond: ["$info.data.archived", 1, 0] } },
|
|
50
|
+
All: { $sum: 1 },
|
|
51
|
+
})
|
|
52
|
+
.project({ _id: 0 })
|
|
53
|
+
.aggregate()
|
|
54
|
+
// .map((e: any) => e.pairs)
|
|
55
|
+
.next(),
|
|
56
|
+
"Total PRs Made": $pipeline(CNRepos)
|
|
57
|
+
.unwind("$crPulls.data")
|
|
58
|
+
.group({ _id: "$crPulls.data.type", total: { $sum: 1 } })
|
|
59
|
+
.sort({ _id: 1 })
|
|
60
|
+
.set({ id_total: [["$_id", "$total"]] })
|
|
61
|
+
.group({ _id: null, pairs: { $mergeObjects: { $arrayToObject: "$id_total" } } })
|
|
62
|
+
.aggregate()
|
|
63
|
+
.map((e: any) => e.pairs)
|
|
64
|
+
.next(),
|
|
65
|
+
"Total Open": $pipeline(CNRepos)
|
|
66
|
+
.unwind("$crPulls.data")
|
|
67
|
+
.match({ "crPulls.data.pull.prState": "open" })
|
|
68
|
+
.group({ _id: "$crPulls.data.type", total: { $sum: 1 } })
|
|
69
|
+
.sort({ _id: 1 })
|
|
70
|
+
.set({ id_total: [["$_id", "$total"]] })
|
|
71
|
+
.group({ _id: null, pairs: { $mergeObjects: { $arrayToObject: "$id_total" } } })
|
|
72
|
+
.aggregate()
|
|
73
|
+
.map((e: any) => e.pairs)
|
|
74
|
+
.next(),
|
|
75
|
+
"Total Merged (on Registry)": $pipeline(CNRepos)
|
|
76
|
+
.unwind("$crPulls.data")
|
|
77
|
+
.match({ cr: { $exists: true }, "crPulls.data.pull.prState": "merged" })
|
|
78
|
+
.group({ _id: "$crPulls.data.type", total: { $sum: 1 } })
|
|
79
|
+
.sort({ _id: 1 })
|
|
80
|
+
.set({ id_total: [["$_id", "$total"]] })
|
|
81
|
+
.group({ _id: null, pairs: { $mergeObjects: { $arrayToObject: "$id_total" } } })
|
|
82
|
+
.aggregate()
|
|
83
|
+
.map((e: any) => e.pairs)
|
|
84
|
+
.next(),
|
|
85
|
+
"Total Merged (not on Registry)": $pipeline(CNRepos)
|
|
86
|
+
.unwind("$crPulls.data")
|
|
87
|
+
.match({ cr: { $exists: false }, "crPulls.data.pull.prState": "merged" })
|
|
88
|
+
.group({ _id: "$crPulls.data.type", total: { $sum: 1 } })
|
|
89
|
+
.sort({ _id: 1 })
|
|
90
|
+
.set({ id_total: [["$_id", "$total"]] })
|
|
91
|
+
.group({ _id: null, pairs: { $mergeObjects: { $arrayToObject: "$id_total" } } })
|
|
92
|
+
.aggregate()
|
|
93
|
+
.map((e: any) => e.pairs)
|
|
94
|
+
.next(),
|
|
95
|
+
"Total Closed": $pipeline(CNRepos)
|
|
96
|
+
.unwind("$crPulls.data")
|
|
97
|
+
.match({ "crPulls.data.pull.prState": "closed" })
|
|
98
|
+
.group({ _id: "$crPulls.data.type", total: { $sum: 1 } })
|
|
99
|
+
.sort({ _id: 1 })
|
|
100
|
+
.set({ id_total: [["$_id", "$total"]] })
|
|
101
|
+
.group({ _id: null, pairs: { $mergeObjects: { $arrayToObject: "$id_total" } } })
|
|
102
|
+
.aggregate()
|
|
103
|
+
.map((e: any) => e.pairs)
|
|
104
|
+
.next(),
|
|
105
|
+
|
|
106
|
+
// // Follow Rules
|
|
107
|
+
// "Follow Up Rules": (async function () {
|
|
108
|
+
// await pMap($pipeline(FollowRuleSets).aggregate(), async (ruleset) => {
|
|
109
|
+
|
|
110
|
+
// });
|
|
111
|
+
// })(),
|
|
112
|
+
});
|
|
113
|
+
return totals;
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import DIE from "@snomiao/die";
|
|
2
|
+
import { $ as bunSh } from "bun";
|
|
3
|
+
import { os } from "zx";
|
|
4
|
+
import { getActivateCMD } from "./cli/getActivateCMD";
|
|
5
|
+
|
|
6
|
+
export async function checkComfyActivated() {
|
|
7
|
+
console.log("Checking ComfyUI Activated...");
|
|
8
|
+
|
|
9
|
+
if (!(await bunSh`comfy --help`.quiet().catch(() => null))) {
|
|
10
|
+
const activate = getActivateCMD();
|
|
11
|
+
// apt-get install -y python3 python3-venv
|
|
12
|
+
const installPython =
|
|
13
|
+
os.platform() === "win32"
|
|
14
|
+
? "python3 --version || winget install python3 || choco install -y python3"
|
|
15
|
+
: "apt-get install -y python3 python3-venv";
|
|
16
|
+
|
|
17
|
+
await bunSh`
|
|
18
|
+
${installPython}
|
|
19
|
+
python -m venv .venv
|
|
20
|
+
${activate}
|
|
21
|
+
pip install comfy-cli
|
|
22
|
+
comfy-cli --help
|
|
23
|
+
`.catch(console.error);
|
|
24
|
+
|
|
25
|
+
DIE(
|
|
26
|
+
`
|
|
27
|
+
Cound not found comfy-cli.
|
|
28
|
+
Please install comfy-cli before run "bunx comfy-pr" here.
|
|
29
|
+
|
|
30
|
+
$ >>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
31
|
+
${installPython}
|
|
32
|
+
python -m venv .venv
|
|
33
|
+
${activate}
|
|
34
|
+
pip install comfy-cli
|
|
35
|
+
comfy-cli --help
|
|
36
|
+
`.trim(),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.test.ts
ADDED
|
File without changes
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!bun
|
|
2
|
+
import DIE from "@snomiao/die";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { argv } from "zx";
|
|
5
|
+
import { checkComfyActivated } from "./checkComfyActivated";
|
|
6
|
+
import { createComfyRegistryPullRequests } from "./createComfyRegistryPullRequests";
|
|
7
|
+
if (argv.help) {
|
|
8
|
+
console.log(
|
|
9
|
+
`
|
|
10
|
+
bunx comfy-pr --repolist repos.txt one repo per-line
|
|
11
|
+
bunx comfy-pr [...GITHUB_REPO_URLS] github repos
|
|
12
|
+
bunx cross-env REPO=https://github.com/OWNER/REPO bunx comfy-pr
|
|
13
|
+
`.trim(),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
await checkComfyActivated();
|
|
19
|
+
|
|
20
|
+
const envRepos =
|
|
21
|
+
process.env.REPO?.split("\n")
|
|
22
|
+
.map((e) => e.trim())
|
|
23
|
+
.filter(Boolean) || [];
|
|
24
|
+
const argvRepos = argv._.filter((a) => !a.endsWith(import.meta.filename));
|
|
25
|
+
const listRepos =
|
|
26
|
+
(argv.repolist &&
|
|
27
|
+
(await readFile(argv.repolist, "utf8").catch(() => ""))
|
|
28
|
+
.split("\n")
|
|
29
|
+
.map((e) => e.trim())
|
|
30
|
+
.filter(Boolean)) ||
|
|
31
|
+
[];
|
|
32
|
+
const repos = (listRepos.length && listRepos) ||
|
|
33
|
+
(argvRepos.length && argvRepos) ||
|
|
34
|
+
(envRepos.length && envRepos) || [
|
|
35
|
+
DIE("Missing PR target, please set env.REPO"),
|
|
36
|
+
];
|
|
37
|
+
for await (const upstreamUrl of repos) {
|
|
38
|
+
await createComfyRegistryPullRequests(upstreamUrl);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { makePublishcrBranch } from "./makePublishBranch";
|
|
2
|
+
import { makePyprojectBranch } from "./makeTomlBranch";
|
|
3
|
+
|
|
4
|
+
export async function clone_modify_push_Branches(
|
|
5
|
+
upstreamUrl: string,
|
|
6
|
+
forkUrl: string,
|
|
7
|
+
) {
|
|
8
|
+
return (
|
|
9
|
+
await Promise.all([
|
|
10
|
+
makePyprojectBranch(upstreamUrl, forkUrl),
|
|
11
|
+
makePublishcrBranch(upstreamUrl, forkUrl),
|
|
12
|
+
])
|
|
13
|
+
).map(({ body, branch, title, type }) => ({
|
|
14
|
+
body,
|
|
15
|
+
branch,
|
|
16
|
+
title,
|
|
17
|
+
type,
|
|
18
|
+
srcUrl: forkUrl,
|
|
19
|
+
dstUrl: upstreamUrl,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
|
|
2
|
+
import pMap from "p-map";
|
|
3
|
+
import { match } from "ts-pattern";
|
|
4
|
+
import { CNRepos } from "./CNRepos";
|
|
5
|
+
import { createComfyRegistryPullRequests } from "./createComfyRegistryPullRequests";
|
|
6
|
+
import { $filaten, $stale } from "./db";
|
|
7
|
+
import { parseUrlRepoOwner, stringifyOwnerRepo } from "./parseOwnerRepo";
|
|
8
|
+
import { notifySlackLinks } from "./slack/notifySlackLinks";
|
|
9
|
+
import { $OK, TaskError, TaskOK } from "./utils/Task";
|
|
10
|
+
import { tLog } from "./utils/tLog";
|
|
11
|
+
if (import.meta.main) {
|
|
12
|
+
await tLog("createComfyRegistryPRsFromCandidates", createComfyRegistryPRsFromCandidates);
|
|
13
|
+
console.log("all done");
|
|
14
|
+
}
|
|
15
|
+
export async function createComfyRegistryPRsFromCandidates() {
|
|
16
|
+
await CNRepos.createIndex($filaten({ candidate: { data: 1 } }));
|
|
17
|
+
await CNRepos.createIndex(
|
|
18
|
+
$filaten({
|
|
19
|
+
candidate: { data: 1 },
|
|
20
|
+
createdPulls: { state: 1, mtime: 1 },
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
return await pMap(
|
|
24
|
+
$pipeline(CNRepos)
|
|
25
|
+
.match(
|
|
26
|
+
$filaten({
|
|
27
|
+
candidate: { data: { $eq: true } },
|
|
28
|
+
createdPulls: { state: { $ne: "ok" }, mtime: $stale("5m") },
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
.aggregate(),
|
|
32
|
+
async (repo) => {
|
|
33
|
+
const { repository } = repo;
|
|
34
|
+
console.log("Making PRs for " + repository);
|
|
35
|
+
const createdPulls = await createComfyRegistryPullRequests(repository).then(TaskOK).catch(TaskError);
|
|
36
|
+
match(createdPulls).with($OK, async ({ data }) => {
|
|
37
|
+
const links = data.map((e) => ({
|
|
38
|
+
href: e.html_url,
|
|
39
|
+
name: stringifyOwnerRepo(parseUrlRepoOwner(e.html_url.replace(/\/pull\/.*$/, ""))) + " #" + e.title,
|
|
40
|
+
}));
|
|
41
|
+
await notifySlackLinks("PR just Created, @HaoHao check plz", links);
|
|
42
|
+
await pMap(data, async (pull) => {
|
|
43
|
+
const { html_url } = pull;
|
|
44
|
+
// also update to crPulls
|
|
45
|
+
await CNRepos.updateOne($filaten({ repository, crPulls: { data: { pull: { html_url } } } }), {
|
|
46
|
+
$set: { "crPulls.data.$.pull": pull },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return await CNRepos.updateOne({ repository }, { $set: { createdPulls } });
|
|
52
|
+
},
|
|
53
|
+
{ concurrency: 2, stopOnError: false },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pMap from "p-map";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
// import { chalk } from "zx";
|
|
4
|
+
import { clone_modify_push_Branches } from "./clone_modify_push_Branches";
|
|
5
|
+
import { createGithubForkForRepo } from "./createGithubForkForRepo";
|
|
6
|
+
import { createGithubPullRequest } from "./createGithubPullRequest";
|
|
7
|
+
import type { GithubPull } from "./gh/GithubPull";
|
|
8
|
+
import { parsePulls } from "./parsePullsState";
|
|
9
|
+
|
|
10
|
+
export async function createComfyRegistryPullRequests(upstreamRepoUrl: string) {
|
|
11
|
+
const forkedRepo = await createGithubForkForRepo(upstreamRepoUrl);
|
|
12
|
+
const PR_REQUESTS = await clone_modify_push_Branches(upstreamRepoUrl, forkedRepo.html_url);
|
|
13
|
+
// branch is ready in fork now
|
|
14
|
+
// create prs for each branch
|
|
15
|
+
console.log("Going to create PRs for the following branches:");
|
|
16
|
+
// console.log(chalk.green(yaml.stringify({ PR_REQUESTS })));
|
|
17
|
+
console.log(yaml.stringify({ PR_REQUESTS }));
|
|
18
|
+
// prs
|
|
19
|
+
const prs = await pMap(PR_REQUESTS, async ({ type, ...prInfo }) => await createGithubPullRequest({ ...prInfo }));
|
|
20
|
+
console.log("ALL PRs DONE");
|
|
21
|
+
return (prs as GithubPull[]).map((e) => parsePulls([e])[0]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import DIE from "@snomiao/die";
|
|
2
|
+
import md5 from "md5";
|
|
3
|
+
import minimist from "minimist";
|
|
4
|
+
import { FORK_OWNER } from "./FORK_OWNER";
|
|
5
|
+
import { FORK_PREFIX } from "./FORK_PREFIX";
|
|
6
|
+
import { createGithubFork } from "./gh/createGithubFork";
|
|
7
|
+
import { ghUser } from "./ghUser";
|
|
8
|
+
import { parseUrlRepoOwner } from "./parseOwnerRepo";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* this function creates a fork of the upstream repo,
|
|
12
|
+
* fork to the FORK_OWNER, and add a prefix to the repo name
|
|
13
|
+
* - the prefix is optional, if not provided, the repo name will be the same as the upstream repo
|
|
14
|
+
* - the prefix is useful to distinguish the forked repo from the other repo in the same owner
|
|
15
|
+
* SALT is used to generate a unique repo name, so that the forked repo will not conflict with other forks
|
|
16
|
+
*
|
|
17
|
+
* @author snomiao <snomiao@gmail.com>
|
|
18
|
+
* @param upstreamRepoUrl
|
|
19
|
+
* @returns forked repo info
|
|
20
|
+
*/
|
|
21
|
+
export async function createGithubForkForRepo(upstreamRepoUrl: string) {
|
|
22
|
+
// debug
|
|
23
|
+
// console.log(`* Change env.SALT=${salt} will fork into a different repo`);
|
|
24
|
+
// console.log("PR_SRC: ", forkSSHUrl);
|
|
25
|
+
// console.log("PR_DST: ", upstreamUrl);
|
|
26
|
+
// console.log(forkSSHUrl);
|
|
27
|
+
const upstream = parseUrlRepoOwner(upstreamRepoUrl);
|
|
28
|
+
const argv = minimist(process.argv.slice(2));
|
|
29
|
+
const salt = argv.salt || process.env.SALT || "m3KMgZ2AeZGWYh7W";
|
|
30
|
+
const repo_hash = md5(`${salt}-${ghUser.name}-${upstream.owner}/${upstream.repo}`).slice(0, 8);
|
|
31
|
+
const forkRepoName = (FORK_PREFIX && `${FORK_PREFIX}${upstream.repo}-${repo_hash}`) || upstream.repo;
|
|
32
|
+
const forkDst = `${FORK_OWNER}/${forkRepoName}`;
|
|
33
|
+
const forkUrl = `https://github.com/${forkDst}`;
|
|
34
|
+
const forked = await createGithubFork(upstreamRepoUrl, forkUrl);
|
|
35
|
+
if (forked.html_url !== forkUrl) DIE("forked url not expected");
|
|
36
|
+
return forked;
|
|
37
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import DIE from "@snomiao/die";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
import { gh } from "./gh";
|
|
4
|
+
import type { GithubPull } from "./gh/GithubPull";
|
|
5
|
+
import { parseUrlRepoOwner } from "./parseOwnerRepo";
|
|
6
|
+
|
|
7
|
+
export async function createGithubPullRequest({
|
|
8
|
+
title,
|
|
9
|
+
body,
|
|
10
|
+
branch,
|
|
11
|
+
srcUrl,
|
|
12
|
+
dstUrl,
|
|
13
|
+
}: {
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
branch: string;
|
|
17
|
+
srcUrl: string;
|
|
18
|
+
dstUrl: string;
|
|
19
|
+
}) {
|
|
20
|
+
const dst = parseUrlRepoOwner(dstUrl);
|
|
21
|
+
const src = parseUrlRepoOwner(srcUrl);
|
|
22
|
+
const repo = (await gh.repos.get({ ...dst })).data;
|
|
23
|
+
|
|
24
|
+
// TODO: seems has bugs on head_repo
|
|
25
|
+
const existedList = (
|
|
26
|
+
await gh.pulls.list({
|
|
27
|
+
// source repo
|
|
28
|
+
state: "all",
|
|
29
|
+
head_repo: src.owner + "/" + src.repo,
|
|
30
|
+
head: src.owner + ":" + branch,
|
|
31
|
+
// pr will merge into
|
|
32
|
+
owner: dst.owner,
|
|
33
|
+
repo: dst.repo,
|
|
34
|
+
base: repo.default_branch,
|
|
35
|
+
})
|
|
36
|
+
).data;
|
|
37
|
+
if (existedList.length) {
|
|
38
|
+
const msg = {
|
|
39
|
+
PR_Existed: existedList.map((e) => ({ url: e.html_url, title: e.title })),
|
|
40
|
+
};
|
|
41
|
+
console.log(yaml.stringify(msg));
|
|
42
|
+
return existedList[0];
|
|
43
|
+
}
|
|
44
|
+
if (!process.env.GH_TOKEN_COMFY_PR) {
|
|
45
|
+
DIE("Missing env.GH_TOKEN_COMFY_PR");
|
|
46
|
+
}
|
|
47
|
+
const pr_result = await gh.pulls
|
|
48
|
+
.create({
|
|
49
|
+
// pr info
|
|
50
|
+
title,
|
|
51
|
+
body,
|
|
52
|
+
// source repo
|
|
53
|
+
head_repo: src.owner + "/" + src.repo,
|
|
54
|
+
head: src.owner + ":" + branch,
|
|
55
|
+
// pr will merge into
|
|
56
|
+
owner: dst.owner,
|
|
57
|
+
repo: dst.repo,
|
|
58
|
+
base: repo.default_branch,
|
|
59
|
+
maintainer_can_modify: true,
|
|
60
|
+
// draft: true,
|
|
61
|
+
})
|
|
62
|
+
.then((e) => e.data)
|
|
63
|
+
.catch(async (e) => {
|
|
64
|
+
if (e.message.match("A pull request already exists for")) {
|
|
65
|
+
console.log("PR Existed ", e);
|
|
66
|
+
// WARN: will search all prs
|
|
67
|
+
const existedList = (
|
|
68
|
+
await gh.pulls.list({
|
|
69
|
+
// source repo
|
|
70
|
+
state: "open",
|
|
71
|
+
head_repo: src.owner + "/" + src.repo,
|
|
72
|
+
// head: src.owner + ":" + branch,
|
|
73
|
+
// pr will merge into
|
|
74
|
+
owner: dst.owner,
|
|
75
|
+
repo: dst.repo,
|
|
76
|
+
base: repo.default_branch,
|
|
77
|
+
})
|
|
78
|
+
).data;
|
|
79
|
+
if (existedList.length) {
|
|
80
|
+
const msg = {
|
|
81
|
+
PR_Existed: existedList.map((e) => ({
|
|
82
|
+
url: e.html_url,
|
|
83
|
+
title: e.title,
|
|
84
|
+
})),
|
|
85
|
+
};
|
|
86
|
+
console.log(yaml.stringify(msg));
|
|
87
|
+
return existedList[0];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
91
|
+
});
|
|
92
|
+
console.log("PR OK", pr_result.html_url);
|
|
93
|
+
return pr_result as GithubPull;
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import DIE from "@snomiao/die";
|
|
2
|
+
import { gh } from "./gh";
|
|
3
|
+
import { ghUser } from "./ghUser";
|
|
4
|
+
import { parseIssueUrl } from "./parseIssueUrl";
|
|
5
|
+
|
|
6
|
+
if (import.meta.main) {
|
|
7
|
+
const url = "https://github.com/snomiao/ComfyNode-Registry-test/pull/1";
|
|
8
|
+
const body = "Hello World @snomiao";
|
|
9
|
+
const result = await createIssueComment(url, body, ghUser.login);
|
|
10
|
+
console.log(result.comment.html_url);
|
|
11
|
+
console.log(result.comments.map((e) => e.html_url));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function createIssueComment(issueUrl: string, body: string, by: string) {
|
|
15
|
+
const comments = (
|
|
16
|
+
await gh.issues.listComments({
|
|
17
|
+
...parseIssueUrl(issueUrl),
|
|
18
|
+
})
|
|
19
|
+
).data;
|
|
20
|
+
const result = await (async function () {
|
|
21
|
+
const commentExisted = comments.find((e) => e.body === body);
|
|
22
|
+
if (commentExisted) return { comment: commentExisted, comments };
|
|
23
|
+
if (by !== ghUser.login) DIE("Fails to creating issue: user not match");
|
|
24
|
+
const comment = (await gh.issues.createComment({ ...parseIssueUrl(issueUrl), body })).data;
|
|
25
|
+
console.log("comment created: " + comment.html_url);
|
|
26
|
+
return { comment, comments: [...comments, comment] };
|
|
27
|
+
})();
|
|
28
|
+
return result;
|
|
29
|
+
}
|