comfy-pr 0.2.20 → 0.2.24

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.
Files changed (78) hide show
  1. package/README.md +225 -225
  2. package/next-env.d.ts +5 -5
  3. package/package.json +10 -2
  4. package/src/Authors.ts +48 -0
  5. package/src/CMNodes.ts +60 -60
  6. package/src/CNRepos.ts +74 -72
  7. package/src/CRNodes.ts +27 -27
  8. package/src/CRPulls.ts +4 -4
  9. package/src/FORK_OWNER.ts +5 -5
  10. package/src/FORK_PREFIX.ts +5 -5
  11. package/src/FollowRules.ts +25 -25
  12. package/src/GIT_USEREMAIL.ts +5 -5
  13. package/src/GIT_USERNAME.ts +9 -9
  14. package/src/GithubIssueComments.ts +3 -3
  15. package/src/PushedBranch.ts +3 -3
  16. package/src/Totals.ts +9 -9
  17. package/src/TrackingPRs.ts +12 -12
  18. package/src/WorkerInstances.ts +89 -89
  19. package/src/addCommentAction.ts +65 -0
  20. package/src/analyzePullsStatus.ts +170 -134
  21. package/src/analyzeTotals.test.ts +5 -5
  22. package/src/analyzeTotals.ts +118 -120
  23. package/src/checkComfyActivated.ts +39 -39
  24. package/src/cli.ts +40 -40
  25. package/src/clone_modify_push_Branches.ts +21 -21
  26. package/src/createComfyRegistryPRsFromCandidates.ts +55 -55
  27. package/src/createComfyRegistryPullRequests.ts +22 -22
  28. package/src/createGithubForkForRepo.ts +37 -37
  29. package/src/createGithubPullRequest.ts +94 -94
  30. package/src/createIssueComment.ts +29 -29
  31. package/src/fetchCMNodes.ts +22 -22
  32. package/src/fetchComfyRegistryNodes.ts +11 -11
  33. package/src/fetchCurrentGeoInfo.ts +6 -6
  34. package/src/fetchRelatedPullWithComments.ts +15 -15
  35. package/src/fetchRepoDescriptionMap.ts +15 -15
  36. package/src/followRuleSchema.ts +76 -76
  37. package/src/getActivatedShell.ts +18 -18
  38. package/src/getBranchWorkingDir.ts +16 -16
  39. package/src/getRepoWorkingDir.ts +5 -5
  40. package/src/ghUser.ts +6 -6
  41. package/src/index.ts +22 -21
  42. package/src/initializeFollowRules.ts +26 -26
  43. package/src/makePublishBranch.ts +58 -58
  44. package/src/makeTomlBranch.ts +53 -53
  45. package/src/matchRelatedPulls.ts +60 -60
  46. package/src/muteInstances.ts +16 -16
  47. package/src/parseIssueUrl.ts +6 -6
  48. package/src/parseOwnerRepo.ts +33 -33
  49. package/src/parsePullUrl.ts +6 -6
  50. package/src/parsePullsState.ts +6 -6
  51. package/src/parseTitleBodyOfMarkdown.ts +8 -8
  52. package/src/postSlackMessage.ts +21 -21
  53. package/src/preload.ts +30 -30
  54. package/src/pullStatusFollowSchema.ts +12 -12
  55. package/src/readTemplateTitle.ts +11 -11
  56. package/src/showFollowRuleSet.ts +12 -0
  57. package/src/summaryLastPullComment.ts +8 -8
  58. package/src/tomlFillDescription.ts +17 -17
  59. package/src/updateAuthors.ts +7 -0
  60. package/src/updateAuthorsForGithub.ts +50 -0
  61. package/src/updateAuthorsFromCNRepo.ts +30 -0
  62. package/src/updateCMNodesDuplicationWarnings.ts +56 -56
  63. package/src/updateCMRepos.ts +27 -27
  64. package/src/updateCNRepos.ts +58 -58
  65. package/src/updateCNReposCRPullsComments.ts +40 -40
  66. package/src/updateCNReposInfo.ts +57 -57
  67. package/src/updateCNReposPRCandidate.ts +85 -85
  68. package/src/updateCNReposPulls.ts +28 -28
  69. package/src/updateCNReposPullsDashboard.ts +49 -49
  70. package/src/updateCNReposRelatedPulls.ts +25 -25
  71. package/src/updateCRNodes.ts +50 -50
  72. package/src/updateCRRepos.ts +26 -26
  73. package/src/updateComfyTotals.ts +43 -43
  74. package/src/updateFollowRuleSet.ts +127 -182
  75. package/src/updateOutdatedPullsTemplates.ts +160 -156
  76. package/src/updateSlackMessages.ts +43 -43
  77. package/tailwind.config.ts +20 -20
  78. package/CHANGELOG.md +0 -118
package/src/CNRepos.ts CHANGED
@@ -1,72 +1,74 @@
1
- #!/usr/bin/env bun
2
- /*
3
- bun index.ts
4
- */
5
- import type { ObjectId, WithId } from "mongodb";
6
- import "react-hook-form";
7
- import { type Task } from "../packages/mongodb-pipeline-ts/Task";
8
- import { type CMNode } from "./CMNodes";
9
- import { type CRNode } from "./CRNodes";
10
- import { db } from "./db";
11
- import { gh } from "./gh";
12
- import { type RelatedPull } from "./matchRelatedPulls";
13
- import type { GithubPullParsed } from "./parsePullsState";
14
- import { type SlackMsg } from "./slack/SlackMsgs";
15
-
16
- type Email = {
17
- from?: string;
18
- title: string;
19
- body: string;
20
- to: string;
21
- };
22
-
23
- type Sent = {
24
- slack?: SlackMsg;
25
- emails?: Email[];
26
- };
27
-
28
- type GithubIssueComment = Awaited<ReturnType<typeof gh.issues.getComment>>["data"];
29
- type GithubIssue = Awaited<ReturnType<typeof gh.issues.get>>["data"];
30
- type GithubRepo = Awaited<ReturnType<typeof gh.repos.get>>["data"];
31
- export type CRType = "pyproject" | "publichcr";
32
- export type CRPull = RelatedPull & {
33
- edited?: Task<boolean>;
34
- comments?: Task<Pick<GithubIssueComment, "body">[]>;
35
- issue?: Task<Pick<GithubIssue, "number" | "html_url" | "body" | "updated_at">>;
36
- };
37
-
38
- export type CustomNodeRepo = {
39
- repository: string;
40
- info?: Task<Pick<GithubRepo, "html_url" | "archived" | "default_branch" | "private">>;
41
-
42
- /** @deprecated use cr_ids or on_registry */
43
- cr?: Pick<WithId<CRNode>, "_id" | "id" | "name">;
44
-
45
- cr_ids?: ObjectId[];
46
- on_registry?: Task<boolean>; // check if cr_ids is not empty
47
-
48
- /** @deprecated use cm_ids */
49
- cm?: Pick<WithId<CMNode>, "_id" | "id" | "title">;
50
- cm_ids?: ObjectId[];
51
-
52
- // cache
53
- pulls?: Task<GithubPullParsed[]>;
54
-
55
- crPulls?: Task<CRPull[]>;
56
-
57
- /** @deprecated TODO: NOT IMPLEMENTD */
58
- crPull_ids: ObjectId[];
59
-
60
- candidate?: Task<boolean>;
61
- // createFork?: Task<GithubRepo>;
62
- // createBranches?: Task<{ type: CRType; assigned: Worker } & PushedBranch>[];
63
-
64
- /** @deprecated use CRPulls.pull */
65
- createdPulls?: Task<GithubPullParsed[]>;
66
- };
67
- export type CNRepo = CustomNodeRepo;
68
- export const CNRepos = db.collection<CNRepo>("CNRepos");
69
- await CNRepos.createIndex({ repository: 1 }, { unique: true });
70
-
71
- // fix cr null, it should be not exists
72
- await CNRepos.updateMany({ cr: null as unknown as WithId<CRNode> }, { $unset: { cr: 1 } });
1
+ #!/usr/bin/env bun
2
+ /*
3
+ bun index.ts
4
+ */
5
+ import type { ObjectId, WithId } from "mongodb";
6
+ import "react-hook-form";
7
+ import { type Task } from "../packages/mongodb-pipeline-ts/Task";
8
+ import { type CMNode } from "./CMNodes";
9
+ import { type CRNode } from "./CRNodes";
10
+ import { db } from "./db";
11
+ import { gh } from "./gh";
12
+ import { type RelatedPull } from "./matchRelatedPulls";
13
+ import type { GithubPullParsed } from "./parsePullsState";
14
+ import { type SlackMsg } from "./slack/SlackMsgs";
15
+
16
+ type Email = {
17
+ from?: string;
18
+ title: string;
19
+ body: string;
20
+ to: string;
21
+ };
22
+
23
+ type Sent = {
24
+ slack?: SlackMsg;
25
+ emails?: Email[];
26
+ };
27
+
28
+ type GithubIssueComment = Awaited<ReturnType<typeof gh.issues.getComment>>["data"];
29
+ type GithubIssue = Awaited<ReturnType<typeof gh.issues.get>>["data"];
30
+ type GithubRepo = Awaited<ReturnType<typeof gh.repos.get>>["data"];
31
+ export type CRType = "pyproject" | "publichcr";
32
+ export type CRPull = RelatedPull & {
33
+ edited?: Task<boolean>;
34
+ comments?: Task<Pick<GithubIssueComment, "body">[]>;
35
+ issue?: Task<Pick<GithubIssue, "number" | "html_url" | "body" | "updated_at">>;
36
+ };
37
+
38
+ export type CustomNodeRepo = {
39
+ repository: string;
40
+ info?: Task<Pick<GithubRepo, "html_url" | "archived" | "default_branch" | "private">>;
41
+
42
+ // author?: Author;
43
+
44
+ /** @deprecated use cr_ids or on_registry */
45
+ cr?: Pick<WithId<CRNode>, "_id" | "id" | "name">;
46
+
47
+ cr_ids?: ObjectId[];
48
+ on_registry?: Task<boolean>; // check if cr_ids is not empty
49
+
50
+ /** @deprecated use cm_ids */
51
+ cm?: Pick<WithId<CMNode>, "_id" | "id" | "title">;
52
+ cm_ids?: ObjectId[];
53
+
54
+ // cache
55
+ pulls?: Task<GithubPullParsed[]>;
56
+
57
+ crPulls?: Task<CRPull[]>;
58
+
59
+ /** @deprecated TODO: NOT IMPLEMENTD */
60
+ crPull_ids: ObjectId[];
61
+
62
+ candidate?: Task<boolean>;
63
+ // createFork?: Task<GithubRepo>;
64
+ // createBranches?: Task<{ type: CRType; assigned: Worker } & PushedBranch>[];
65
+
66
+ /** @deprecated use CRPulls.pull */
67
+ createdPulls?: Task<GithubPullParsed[]>;
68
+ };
69
+ export type CNRepo = CustomNodeRepo;
70
+ export const CNRepos = db.collection<CNRepo>("CNRepos");
71
+ await CNRepos.createIndex({ repository: 1 }, { unique: true });
72
+
73
+ // fix cr null, it should be not exists
74
+ await CNRepos.updateMany({ cr: null as unknown as WithId<CRNode> }, { $unset: { cr: 1 } });
package/src/CRNodes.ts CHANGED
@@ -1,27 +1,27 @@
1
- import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
2
- import type { ObjectId } from "mongodb";
3
- import pMap from "p-map";
4
- import { peekYaml } from "peek-log";
5
- import { CNRepos } from "./CNRepos";
6
- import { db } from "./db";
7
- import { fetchCRNodes } from "./fetchComfyRegistryNodes";
8
- import { type SlackMsg } from "./slack/SlackMsgs";
9
-
10
- export type CRNode = Awaited<ReturnType<typeof fetchCRNodes>>[number] & {
11
- sent?: { slack?: SlackMsg };
12
- repo_id?: ObjectId;
13
- };
14
- export const CRNodes = db.collection<CRNode>("CRNodes");
15
- await CRNodes.createIndex({ id: 1 }, { unique: true });
16
- await CRNodes.createIndex({ repository: 1 }, { unique: false }); // WARN: duplicate is allowed
17
-
18
- if (import.meta.main) {
19
- const r = await pMap(
20
- $pipeline(CNRepos)
21
- .match({ cr: { $exists: true } })
22
- .replaceRoot({ newRoot: "$cr" })
23
- .aggregate(),
24
- (e) => e,
25
- );
26
- peekYaml({ r, len: r.length });
27
- }
1
+ import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
2
+ import type { ObjectId } from "mongodb";
3
+ import pMap from "p-map";
4
+ import { peekYaml } from "peek-log";
5
+ import { CNRepos } from "./CNRepos";
6
+ import { db } from "./db";
7
+ import { fetchCRNodes } from "./fetchComfyRegistryNodes";
8
+ import { type SlackMsg } from "./slack/SlackMsgs";
9
+
10
+ export type CRNode = Awaited<ReturnType<typeof fetchCRNodes>>[number] & {
11
+ sent?: { slack?: SlackMsg };
12
+ repo_id?: ObjectId;
13
+ };
14
+ export const CRNodes = db.collection<CRNode>("CRNodes");
15
+ await CRNodes.createIndex({ id: 1 }, { unique: true });
16
+ await CRNodes.createIndex({ repository: 1 }, { unique: false }); // WARN: duplicate is allowed
17
+
18
+ if (import.meta.main) {
19
+ const r = await pMap(
20
+ $pipeline(CNRepos)
21
+ .match({ cr: { $exists: true } })
22
+ .replaceRoot({ newRoot: "$cr" })
23
+ .aggregate(),
24
+ (e) => e,
25
+ );
26
+ peekYaml({ r, len: r.length });
27
+ }
package/src/CRPulls.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CRPull } from "./CNRepos";
2
- import { db } from "./db";
3
-
4
- export const CRPulls = db.collection<CRPull>("CRPulls");
1
+ import type { CRPull } from "./CNRepos";
2
+ import { db } from "./db";
3
+
4
+ export const CRPulls = db.collection<CRPull>("CRPulls");
package/src/FORK_OWNER.ts CHANGED
@@ -1,5 +1,5 @@
1
- import DIE from "@snomiao/die";
2
- import { ghUser } from "./ghUser";
3
-
4
- export const FORK_OWNER =
5
- process.env.FORK_OWNER?.replace(/"/g, "")?.trim() || ghUser.login || DIE("Missing env.FORK_OWNER");
1
+ import DIE from "@snomiao/die";
2
+ import { ghUser } from "./ghUser";
3
+
4
+ export const FORK_OWNER =
5
+ process.env.FORK_OWNER?.replace(/"/g, "")?.trim() || ghUser.login || DIE("Missing env.FORK_OWNER");
@@ -1,5 +1,5 @@
1
- import DIE from "@snomiao/die";
2
-
3
- export const FORK_PREFIX =
4
- process.env.FORK_PREFIX?.replace(/"/g, "")?.trim() ||
5
- DIE('Missing env.FORK_PREFIX, if you want empty maybe try FORK_PREFIX=""');
1
+ import DIE from "@snomiao/die";
2
+
3
+ export const FORK_PREFIX =
4
+ process.env.FORK_PREFIX?.replace(/"/g, "")?.trim() ||
5
+ DIE('Missing env.FORK_PREFIX, if you want empty maybe try FORK_PREFIX=""');
@@ -1,25 +1,25 @@
1
- import { z } from "zod";
2
- import { type Task } from "../packages/mongodb-pipeline-ts/Task";
3
- import type { PullsStatus } from "./analyzePullsStatus";
4
- import { db } from "./db";
5
- import type { zFollowUpRules } from "./followRuleSchema";
6
-
7
- // migrate data
8
- // await db.renameCollection("FollowRules", "FollowRuleSets").catch(() => null);
9
- export type FollowRule = z.infer<typeof zFollowUpRules>[number];
10
- export type FollowRuleSet = {
11
- // rules?: Task<FollowRule[]>;
12
- name: string;
13
- yaml: string;
14
- rules?: FollowRule[];
15
- matched?: Task<PullsStatus>;
16
- action_results?: {
17
- name: string;
18
- action: any;
19
- result: Task<any>;
20
- }[];
21
- enabled?: boolean;
22
- yamlWhenEnabled?: string;
23
- };
24
- export const FollowRuleSets = db.collection<FollowRuleSet>("FollowRuleSets");
25
- await FollowRuleSets.createIndex("name", { unique: true });
1
+ import { z } from "zod";
2
+ import { type Task } from "../packages/mongodb-pipeline-ts/Task";
3
+ import type { PullsStatus } from "./analyzePullsStatus";
4
+ import { db } from "./db";
5
+ import type { zFollowUpRules } from "./followRuleSchema";
6
+
7
+ // migrate data
8
+ // await db.renameCollection("FollowRules", "FollowRuleSets").catch(() => null);
9
+ export type FollowRule = z.infer<typeof zFollowUpRules>[number];
10
+ export type FollowRuleSet = {
11
+ // rules?: Task<FollowRule[]>;
12
+ name: string;
13
+ yaml: string;
14
+ rules?: FollowRule[];
15
+ matched?: Task<PullsStatus>;
16
+ action_results?: {
17
+ name: string;
18
+ action: any;
19
+ result: Task<any>;
20
+ }[];
21
+ enabled?: boolean;
22
+ yamlWhenEnabled?: string;
23
+ };
24
+ export const FollowRuleSets = db.collection<FollowRuleSet>("FollowRuleSets");
25
+ await FollowRuleSets.createIndex("name", { unique: true });
@@ -1,5 +1,5 @@
1
- import DIE from "@snomiao/die";
2
- import { ghUser } from "./ghUser";
3
-
4
- export const GIT_USEREMAIL =
5
- process.env.GIT_USEREMAIL || (ghUser.email && ghUser.email) || DIE("Missing env.GIT_USEREMAIL");
1
+ import DIE from "@snomiao/die";
2
+ import { ghUser } from "./ghUser";
3
+
4
+ export const GIT_USEREMAIL =
5
+ process.env.GIT_USEREMAIL || (ghUser.email && ghUser.email) || DIE("Missing env.GIT_USEREMAIL");
@@ -1,9 +1,9 @@
1
- import DIE from "@snomiao/die";
2
- import { GIT_USEREMAIL } from "./GIT_USEREMAIL";
3
- import { ghUser } from "./ghUser";
4
-
5
- export const GIT_USERNAME =
6
- process.env.GIT_USERNAME || (ghUser.email && ghUser.name) || DIE("Missing env.GIT_USERNAME");
7
-
8
- // read env/parameters
9
- console.log(`GIT COMMIT USER: ${GIT_USERNAME} <${GIT_USEREMAIL}>`);
1
+ import DIE from "@snomiao/die";
2
+ import { GIT_USEREMAIL } from "./GIT_USEREMAIL";
3
+ import { ghUser } from "./ghUser";
4
+
5
+ export const GIT_USERNAME =
6
+ process.env.GIT_USERNAME || (ghUser.email && ghUser.name) || DIE("Missing env.GIT_USERNAME");
7
+
8
+ // read env/parameters
9
+ console.log(`GIT COMMIT USER: ${GIT_USERNAME} <${GIT_USEREMAIL}>`);
@@ -1,3 +1,3 @@
1
- import { fetchIssueComments } from "./gh/fetchIssueComments";
2
-
3
- export type GithubIssueComment = Awaited<ReturnType<typeof fetchIssueComments>>[number];
1
+ import { fetchIssueComments } from "./gh/fetchIssueComments";
2
+
3
+ export type GithubIssueComment = Awaited<ReturnType<typeof fetchIssueComments>>[number];
@@ -1,3 +1,3 @@
1
- import { makePublishcrBranch } from "./makePublishBranch";
2
-
3
- export type PushedBranch = Awaited<ReturnType<typeof makePublishcrBranch>>;
1
+ import { makePublishcrBranch } from "./makePublishBranch";
2
+
3
+ export type PushedBranch = Awaited<ReturnType<typeof makePublishcrBranch>>;
package/src/Totals.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { type Task } from "../packages/mongodb-pipeline-ts/Task";
2
- import { analyzeTotals } from "./analyzeTotals";
3
- import { db } from "./db";
4
- import { type AwaitedReturnType } from "./types/AwaitedReturnType";
5
- export type Totals = AwaitedReturnType<typeof analyzeTotals>;
6
- export const Totals = db.collection<{
7
- today?: string;
8
- totals?: Task<Totals>;
9
- }>("Totals");
1
+ import { type Task } from "../packages/mongodb-pipeline-ts/Task";
2
+ import { analyzeTotals } from "./analyzeTotals";
3
+ import { db } from "./db";
4
+ import { type AwaitedReturnType } from "./types/AwaitedReturnType";
5
+ export type Totals = AwaitedReturnType<typeof analyzeTotals>;
6
+ export const Totals = db.collection<{
7
+ today?: string;
8
+ totals?: Task<Totals>;
9
+ }>("Totals");
@@ -1,12 +1,12 @@
1
- import { db } from "./db";
2
-
3
- // 2) tracking the statuses of the PRs we've sent out (edited)
4
- // "status [open, closed, merged]",
5
- // "comments [int]",
6
- // "last active [date]",
7
- // "last updated [date]",
8
-
9
- export const TrackingPulls = db.collection("TrackingPulls");
10
- // bun --env-file .env.production.local src/dump.ts > dump.log
11
- if (import.meta.main) {
12
- }
1
+ import { db } from "./db";
2
+
3
+ // 2) tracking the statuses of the PRs we've sent out (edited)
4
+ // "status [open, closed, merged]",
5
+ // "comments [int]",
6
+ // "last active [date]",
7
+ // "last updated [date]",
8
+
9
+ export const TrackingPulls = db.collection("TrackingPulls");
10
+ // bun --env-file .env.production.local src/dump.ts > dump.log
11
+ if (import.meta.main) {
12
+ }
@@ -1,89 +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
- }
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,65 @@
1
+ "use server";
2
+ import { $elemMatch } from "@/packages/mongodb-pipeline-ts/$elemMatch";
3
+ import { $pipeline } from "@/packages/mongodb-pipeline-ts/$pipeline";
4
+ import { TaskDataOrNull, type Task } from "@/packages/mongodb-pipeline-ts/Task";
5
+ import DIE from "@snomiao/die";
6
+ import pMap from "p-map";
7
+ import { CNRepos, type CRPull } from "./CNRepos";
8
+ import { createIssueComment } from "./createIssueComment";
9
+ import { $filaten } from "./db";
10
+ import { ghUser } from "./ghUser";
11
+ import type { GithubIssueComment } from "./GithubIssueComments";
12
+ import { notifySlackLinks } from "./slack/notifySlackLinks";
13
+
14
+ export async function addCommentAction({ matched, action, runAction, rule }: { matched: { state: "ok"; mtime: Date; data: { lastwords: string; email: string | ""; comments: number; head: string; on_registry: boolean; comments_author: string; ownername: string; nickName: string; repository: string; state: "OPEN" | "MERGED" | "CLOSED"; url: string; updated: string; pull_updated: string; repo_updated: string; }[]; } | { state: "error"; mtime: Date; error: string; }; action: { by: string; body: string; }; runAction: boolean; rule: { name: string; $match: { actived_at?: Date | { $lt: Date; } | { $gte: Date; } | { $not: { $gt: Date; }; } | undefined; email?: string | RegExp | undefined; comments?: number | { $gt: number; } | { $lt: number; } | { $eq: number; } | { $ne: number; } | { $gte: number; } | { $lte: number; } | undefined; created_at?: Date | { $lt: Date; } | { $gte: Date; } | { $not: { $gt: Date; }; } | undefined; comments_author?: string | RegExp | undefined; head?: string | RegExp | undefined; lastwords?: string | RegExp | undefined; on_registry?: boolean | undefined; nickName?: string | RegExp | undefined; ownername?: string | RegExp | undefined; state?: "OPEN" | "CLOSED" | "MERGED" | undefined; updated_at?: Date | { $lt: Date; } | { $gte: Date; } | { $not: { $gt: Date; }; } | undefined; url?: string | RegExp | undefined; }; action: { "add-comment"?: { by: string; body: string; } | undefined; "send-email"?: { body: string; provider: string; from: string; to: string; subject: string; } | undefined; "update-issue"?: { tags: string | number | RegExp | { $lt: Date; } | { $gte: Date; } | { $nin: string; }; } | undefined; }; }; }) {
15
+ return await pMap(
16
+ TaskDataOrNull(matched) ?? DIE("NO-PAYLOAD-AVAILABLE"),
17
+ async (payload) => {
18
+ const loadedAction = {
19
+ action: "add-comment",
20
+ url: payload.url,
21
+ by: action.by,
22
+ body: action.body.replace(
23
+ /{{\$(\w+)}}/,
24
+ (_, key: string) => (payload as any)[key] ||
25
+ DIE("Missing key: " + key + " in payload: " + JSON.stringify(payload))
26
+ ),
27
+ };
28
+
29
+ if (runAction && loadedAction.by === ghUser.login) {
30
+ const existedCommentsTask = (await $pipeline(CNRepos)
31
+ .unwind("$crPulls.data")
32
+ .match({ "crPulls.data.pull.html_url": loadedAction.url })
33
+ .with<{ "crPulls.data": CRPull; }>()
34
+ .replaceRoot({ newRoot: "$crPulls.data.comments" })
35
+ .as<Task<GithubIssueComment[]>>()
36
+ .aggregate()
37
+ .next()) ??
38
+ DIE("comments is not fetched before, plz check " + loadedAction.url + " in CNRepos");
39
+
40
+ const existedComments = TaskDataOrNull(existedCommentsTask) ??
41
+ DIE("NO-COMMENTS-FOUND should never happen here, bcz pipeline filtered at first");
42
+ const existedComment = existedComments.find((e) => e.body === loadedAction.body);
43
+
44
+ if (!existedComment) {
45
+ const { comments, comment } = await createIssueComment(
46
+ loadedAction.url,
47
+ loadedAction.body,
48
+ loadedAction.by
49
+ );
50
+ const updateResult = await CNRepos.updateOne(
51
+ $filaten({ crPulls: { data: $elemMatch({ pull: { html_url: loadedAction.url } }) } }),
52
+ { $set: { "crPulls.data.$.comments": comments } }
53
+ );
54
+ if (!updateResult.matchedCount) DIE("created issue not matched");
55
+ await notifySlackLinks("A New issue comment are created from rule " + rule.name, [
56
+ comment.html_url,
57
+ ]);
58
+ }
59
+ }
60
+
61
+ return loadedAction;
62
+ },
63
+ { concurrency: 1 }
64
+ );
65
+ }