@shivaedev/pr-watch 0.1.2 → 0.3.0
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 +23 -0
- package/README.md +20 -5
- package/dist/GitHub.d.ts.map +1 -1
- package/dist/GitHub.js +5 -19
- package/dist/GitHub.js.map +1 -1
- package/dist/GitHubHttp.d.ts +5 -6
- package/dist/GitHubHttp.d.ts.map +1 -1
- package/dist/GitHubHttp.js +34 -29
- package/dist/GitHubHttp.js.map +1 -1
- package/dist/Watcher.d.ts +1 -1
- package/dist/Watcher.d.ts.map +1 -1
- package/dist/Watcher.js +4 -4
- package/dist/Watcher.js.map +1 -1
- package/dist/arguments.d.ts +4 -4
- package/dist/arguments.d.ts.map +1 -1
- package/dist/arguments.js +19 -3
- package/dist/arguments.js.map +1 -1
- package/dist/cli.js +13 -12
- package/dist/cli.js.map +1 -1
- package/dist/confirmQueue.d.ts +3 -2
- package/dist/confirmQueue.d.ts.map +1 -1
- package/dist/confirmQueue.js +2 -2
- package/dist/confirmQueue.js.map +1 -1
- package/dist/events.d.ts +4 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -2
- package/dist/events.js.map +1 -1
- package/dist/health.d.ts +6 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +19 -0
- package/dist/health.js.map +1 -0
- package/dist/model.d.ts +3 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -0
- package/dist/model.js.map +1 -1
- package/dist/program.d.ts +3 -2
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +42 -23
- package/dist/program.js.map +1 -1
- package/dist/quota.d.ts +3 -9
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +10 -30
- package/dist/quota.js.map +1 -1
- package/package.json +5 -4
- package/src/GitHub.ts +5 -19
- package/src/GitHubHttp.ts +47 -31
- package/src/Watcher.ts +5 -5
- package/src/arguments.ts +20 -3
- package/src/cli.ts +19 -16
- package/src/confirmQueue.ts +2 -2
- package/src/events.ts +11 -6
- package/src/health.ts +26 -0
- package/src/model.ts +3 -1
- package/src/program.ts +56 -26
- package/src/quota.ts +16 -37
- package/dist/httpResponse.d.ts +0 -15
- package/dist/httpResponse.d.ts.map +0 -1
- package/dist/httpResponse.js +0 -31
- package/dist/httpResponse.js.map +0 -1
- package/src/httpResponse.ts +0 -37
package/dist/events.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { Snapshot } from "
|
|
2
|
-
export declare
|
|
1
|
+
import type { Snapshot } from "#model.ts";
|
|
2
|
+
export declare const eventKinds: readonly ["merged", "closed", "conflict", "queued", "dequeued", "auto-merge-off", "checks-failed", "stuck"];
|
|
3
|
+
export type EventKind = (typeof eventKinds)[number];
|
|
4
|
+
export declare function events(before: Snapshot | undefined, after: Snapshot, selected?: readonly EventKind[]): readonly string[];
|
|
3
5
|
//# sourceMappingURL=events.d.ts.map
|
package/dist/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,eAAO,MAAM,UAAU,YAAI,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AACtI,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAgBpD,wBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,MAAM,EAAE,CAkBxH"}
|
package/dist/events.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const eventKinds = ["merged", "closed", "conflict", "queued", "dequeued", "auto-merge-off", "checks-failed", "stuck"];
|
|
1
2
|
function stuck(pr) {
|
|
2
3
|
return pr.state === "open" && pr.autoMerge && (pr.dirty || pr.failed);
|
|
3
4
|
}
|
|
@@ -12,7 +13,7 @@ function openEvents(before, after) {
|
|
|
12
13
|
];
|
|
13
14
|
return transitions.flatMap(([changed, event]) => (changed ? [event] : []));
|
|
14
15
|
}
|
|
15
|
-
export function events(before, after) {
|
|
16
|
+
export function events(before, after, selected) {
|
|
16
17
|
if (before === undefined && !stuck(after)) {
|
|
17
18
|
return [];
|
|
18
19
|
}
|
|
@@ -27,6 +28,8 @@ export function events(before, after) {
|
|
|
27
28
|
if (after.state === "open") {
|
|
28
29
|
found.push(...openEvents(baseline, after));
|
|
29
30
|
}
|
|
30
|
-
return found
|
|
31
|
+
return found
|
|
32
|
+
.filter((event) => selected === undefined || selected.includes(event))
|
|
33
|
+
.map((event) => JSON.stringify({ branch: after.branch, event, number: after.number, title: after.title }));
|
|
31
34
|
}
|
|
32
35
|
//# sourceMappingURL=events.js.map
|
package/dist/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,SAAS,KAAK,CAAC,EAAY;IAC1B,OAAO,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AACD,SAAS,UAAU,CAAC,MAAgB,EAAE,KAAe;IACpD,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,OAAO,CAAU,CAAC;AAGtI,SAAS,KAAK,CAAC,EAAY;IAC1B,OAAO,EAAE,CAAC,KAAK,KAAK,MAAM,IAAI,EAAE,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AACD,SAAS,UAAU,CAAC,MAAgB,EAAE,KAAe;IACpD,MAAM,WAAW,GAA+C;QAC/D,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;QAC1C,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC;QAC1C,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC;QAC5F,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC;QACjD,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,MAAM,CAAC,MAA4B,EAAE,KAAe,EAAE,QAA+B;IACpG,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACvF,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SACrE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAC7G,CAAC"}
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { WatchError } from "#model.ts";
|
|
3
|
+
export type Emit = (line: string) => Effect.Effect<void, WatchError>;
|
|
4
|
+
export declare const health: (emit: Emit, event: "cooldown" | "error" | "heartbeat" | "stopped", fields?: Record<string, unknown> | undefined) => Effect.Effect<void, WatchError, never>;
|
|
5
|
+
export declare const waitForPoll: (emit: Emit, interval: number, delay: number) => Effect.Effect<void, WatchError, never>;
|
|
6
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AACrE,eAAO,MAAM,MAAM,6JAOjB,CAAC;AAEH,eAAO,MAAM,WAAW,yFAYtB,CAAC"}
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
export const health = Effect.fnUntraced(function* (emit, event, fields = {}) {
|
|
3
|
+
const at = new Date(yield* Clock.currentTimeMillis).toISOString();
|
|
4
|
+
yield* emit(JSON.stringify({ at, event, ...fields }));
|
|
5
|
+
});
|
|
6
|
+
export const waitForPoll = Effect.fnUntraced(function* (emit, interval, delay) {
|
|
7
|
+
const next = (yield* Clock.currentTimeMillis) + delay;
|
|
8
|
+
const nextPollAt = new Date(next).toISOString();
|
|
9
|
+
if (delay > interval) {
|
|
10
|
+
yield* health(emit, "cooldown", { delayMs: delay, message: "GitHub quota pacing", nextPollAt });
|
|
11
|
+
}
|
|
12
|
+
let remaining = delay;
|
|
13
|
+
while (remaining > 0) {
|
|
14
|
+
yield* health(emit, "heartbeat", { nextPollAt });
|
|
15
|
+
yield* Effect.sleep(Math.min(interval, remaining));
|
|
16
|
+
remaining = next - (yield* Clock.currentTimeMillis);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIvC,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAChD,IAAU,EACV,KAAqD,EACrD,MAAM,GAA4B,EAAE;IAEpC,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,WAAW,EAAE,CAAC;IAClE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,IAAU,EAAE,QAAgB,EAAE,KAAa;IAClG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,KAAK,CAAC;IACtD,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACtB,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,OAAO,SAAS,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QACjD,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;QACnD,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrD,CAAC;AACF,CAAC,CAAC,CAAC"}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import type { EventKind } from "#events.ts";
|
|
2
3
|
declare const WatchError_base: Schema.Class<WatchError, Schema.TaggedStruct<"WatchError", {
|
|
3
4
|
readonly message: Schema.String;
|
|
5
|
+
readonly retryAt: Schema.optionalKey<Schema.Number>;
|
|
4
6
|
}>, import("effect/Cause").YieldableError>;
|
|
5
7
|
export declare class WatchError extends WatchError_base {
|
|
6
8
|
}
|
|
@@ -35,12 +37,12 @@ export declare const State: Schema.Struct<{
|
|
|
35
37
|
export type State = typeof State.Type;
|
|
36
38
|
export interface Options {
|
|
37
39
|
readonly apiUrl: string;
|
|
40
|
+
readonly events?: readonly EventKind[];
|
|
38
41
|
readonly fleet: boolean;
|
|
39
42
|
readonly interval: number;
|
|
40
43
|
readonly numbers: readonly number[];
|
|
41
44
|
readonly repository: string;
|
|
42
45
|
readonly stateFile: string;
|
|
43
|
-
readonly token: string;
|
|
44
46
|
}
|
|
45
47
|
export {};
|
|
46
48
|
//# sourceMappingURL=model.d.ts.map
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;;;;;AAC5C,qBAAa,UAAW,SAAQ,eAG9B;CAAG;AACL,eAAO,MAAM,QAAQ;;;;;;;;;;EAUnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAC;AAC5C,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;EAKhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC;AACtC,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B"}
|
package/dist/model.js
CHANGED
package/dist/model.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,WAAW,EAAc,CAAC,YAAY,EAAE;IAC9E,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;CAC1C,CAAC;CAAG;AACL,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC,OAAO;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC,OAAO;IACrB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC,MAAM;IACzB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;CAC1B,CAAC,CAAC"}
|
package/dist/program.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import type
|
|
3
|
-
|
|
2
|
+
import { type Emit } from "#health.ts";
|
|
3
|
+
import type { WatchError } from "#model.ts";
|
|
4
|
+
export declare const program: (args: readonly string[], emit: Emit, diagnostic: (message: string) => Effect.Effect<void, WatchError>) => Effect.Effect<void, WatchError, "@shivaedev/pr-watch/Watcher">;
|
|
4
5
|
//# sourceMappingURL=program.d.ts.map
|
package/dist/program.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,KAAK,IAAI,EAAuB,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAG5C,eAAO,MAAM,OAAO,8DAGG,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,mEAsD/D,CAAC"}
|
package/dist/program.js
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { argumentsFor, help } from "
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { Effect, Exit } from "effect";
|
|
2
|
+
import { argumentsFor, help } from "#arguments.ts";
|
|
3
|
+
import { health, waitForPoll } from "#health.ts";
|
|
4
|
+
import { Watcher } from "#Watcher.ts";
|
|
5
|
+
export const program = Effect.fn("PrWatch.program")(function* (args, emit, diagnostic) {
|
|
6
|
+
let reported;
|
|
7
|
+
let watching = true;
|
|
8
|
+
const run = Effect.gen(function* () {
|
|
9
|
+
const parsed = yield* argumentsFor(args);
|
|
10
|
+
if (parsed.help) {
|
|
11
|
+
watching = false;
|
|
12
|
+
yield* emit(help);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const watcher = yield* Watcher;
|
|
16
|
+
let state = yield* watcher.load(parsed.options);
|
|
17
|
+
function report(message) {
|
|
18
|
+
return health(emit, "error", { message }).pipe(Effect.tap(Effect.sync(() => {
|
|
19
|
+
reported = message;
|
|
20
|
+
})), Effect.andThen(diagnostic(message)));
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
while (true) {
|
|
23
|
+
reported = undefined;
|
|
24
|
+
state = yield* watcher.poll(parsed.options, state, emit, report);
|
|
18
25
|
if (parsed.once) {
|
|
19
|
-
return
|
|
26
|
+
return;
|
|
20
27
|
}
|
|
21
|
-
yield*
|
|
22
|
-
state = yield* watcher.load(parsed.options);
|
|
28
|
+
yield* waitForPoll(emit, parsed.options.interval, yield* watcher.delay(parsed.options.interval));
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
});
|
|
31
|
+
yield* run.pipe(Effect.tapError((error) => Effect.gen(function* () {
|
|
32
|
+
if (reported !== error.message) {
|
|
33
|
+
yield* health(emit, "error", { message: error.message });
|
|
34
|
+
}
|
|
35
|
+
if (error.retryAt !== undefined) {
|
|
36
|
+
yield* health(emit, "cooldown", { message: error.message, nextPollAt: new Date(error.retryAt).toISOString() });
|
|
37
|
+
}
|
|
38
|
+
})), Effect.onExit((exit) => {
|
|
39
|
+
if (!watching) {
|
|
40
|
+
return Effect.void;
|
|
41
|
+
}
|
|
42
|
+
let reason = "completed";
|
|
43
|
+
if (exit._tag === "Failure") {
|
|
44
|
+
reason = Exit.hasInterrupts(exit) ? "interrupted" : "failed";
|
|
26
45
|
}
|
|
27
|
-
|
|
28
|
-
}
|
|
46
|
+
return health(emit, "stopped", { reason }).pipe(Effect.orDie);
|
|
47
|
+
}));
|
|
29
48
|
});
|
|
30
49
|
//# sourceMappingURL=program.js.map
|
package/dist/program.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"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"}
|
package/dist/quota.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
limit: number;
|
|
5
|
-
remaining: number;
|
|
6
|
-
reset: number;
|
|
7
|
-
}
|
|
8
|
-
export declare function observeBudget(budget: Budget, response: Pick<HttpClientResponse.HttpClientResponse, "headers" | "status">, now: number): void;
|
|
9
|
-
export declare function pollDelay(budget: Budget, interval: number, requests: number, now: number): number;
|
|
1
|
+
import type { RateLimitObservation } from "@shivaedev/effect-github/rateLimits.ts";
|
|
2
|
+
export declare function quotaRetryAt(observations: readonly RateLimitObservation[], retryAt?: number): number;
|
|
3
|
+
export declare function pollDelay(observations: readonly RateLimitObservation[], interval: number, requests: number, now: number, retryAt?: number): number;
|
|
10
4
|
//# sourceMappingURL=quota.d.ts.map
|
package/dist/quota.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAEnF,wBAAgB,YAAY,CAAC,YAAY,EAAE,SAAS,oBAAoB,EAAE,EAAE,OAAO,SAAI,GAAG,MAAM,CAE/F;AAED,wBAAgB,SAAS,CAAC,YAAY,EAAE,SAAS,oBAAoB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,MAAM,CAW7I"}
|
package/dist/quota.js
CHANGED
|
@@ -1,34 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return
|
|
1
|
+
import { DateTime } from "effect";
|
|
2
|
+
export function quotaRetryAt(observations, retryAt = 0) {
|
|
3
|
+
return Math.max(retryAt, ...observations.filter((budget) => budget.remaining === 0).map((budget) => DateTime.toEpochMillis(budget.reset)));
|
|
4
4
|
}
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
budget.blockedUntil = Math.max(budget.blockedUntil, Number.isFinite(until) ? until : now + 60_000);
|
|
13
|
-
}
|
|
14
|
-
if (budget.remaining === 0) {
|
|
15
|
-
budget.blockedUntil = Math.max(budget.blockedUntil, budget.reset);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export function pollDelay(budget, interval, requests, now) {
|
|
19
|
-
const ratio = budget.remaining / Math.max(1, budget.limit);
|
|
20
|
-
const multiplier = slowdown(ratio);
|
|
21
|
-
const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, budget.reset - now) * requests) / budget.remaining) : 0;
|
|
22
|
-
return Math.max(interval * multiplier, paced, budget.blockedUntil - now);
|
|
23
|
-
}
|
|
24
|
-
function retryUntil(retry, now) {
|
|
25
|
-
if (retry === null) {
|
|
26
|
-
return now + 60_000;
|
|
27
|
-
}
|
|
28
|
-
if (Number.isFinite(Number(retry))) {
|
|
29
|
-
return now + Number(retry) * 1000;
|
|
30
|
-
}
|
|
31
|
-
return Date.parse(retry);
|
|
5
|
+
export function pollDelay(observations, interval, requests, now, retryAt = 0) {
|
|
6
|
+
return Math.max(interval, quotaRetryAt(observations, retryAt) - now, ...observations.map((budget) => {
|
|
7
|
+
const ratio = budget.remaining / Math.max(1, budget.limit);
|
|
8
|
+
const reset = DateTime.toEpochMillis(budget.reset);
|
|
9
|
+
const paced = budget.remaining > 0 && ratio < 0.5 ? Math.ceil((Math.max(0, reset - now) * requests) / budget.remaining) : 0;
|
|
10
|
+
return Math.max(interval * slowdown(ratio), paced);
|
|
11
|
+
}));
|
|
32
12
|
}
|
|
33
13
|
function slowdown(ratio) {
|
|
34
14
|
if (ratio < 0.1) {
|
package/dist/quota.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quota.js","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"quota.js","sourceRoot":"","sources":["../src/quota.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGlC,MAAM,UAAU,YAAY,CAAC,YAA6C,EAAE,OAAO,GAAG,CAAC;IACtF,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC5I,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,YAA6C,EAAE,QAAgB,EAAE,QAAgB,EAAE,GAAW,EAAE,OAAO,GAAG,CAAC;IACpI,OAAO,IAAI,CAAC,GAAG,CACd,QAAQ,EACR,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,GAAG,EACzC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5H,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AACD,SAAS,QAAQ,CAAC,KAAa;IAC9B,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACX,CAAC;IACD,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC;IACV,CAAC;IACD,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC;IACV,CAAC;IACD,OAAO,CAAC,CAAC;AACV,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/pr-watch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "One REST watcher for pull request transitions across a repository",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/pr-watch#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,15 +43,16 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@shivaedev/effect-github": "0.3.0",
|
|
46
47
|
"@shivaedev/effect-service": "0.4.1"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@effect/platform-node": "4.0.5",
|
|
50
51
|
"@effect/platform-node-shared": "4.0.5",
|
|
51
52
|
"@effect/vitest": "4.0.5",
|
|
52
|
-
"@shivaedev/effect-test": "0.
|
|
53
|
-
"@shivaedev/quality": "0.
|
|
54
|
-
"@shivaedev/test-story": "0.
|
|
53
|
+
"@shivaedev/effect-test": "0.8.1",
|
|
54
|
+
"@shivaedev/quality": "0.39.0",
|
|
55
|
+
"@shivaedev/test-story": "0.19.0",
|
|
55
56
|
"@types/node": "24.19.2",
|
|
56
57
|
"effect": "4.0.5",
|
|
57
58
|
"typescript": "7.0.2",
|
package/src/GitHub.ts
CHANGED
|
@@ -28,21 +28,7 @@ export const GitHub = defineService({
|
|
|
28
28
|
return Schema.decodeUnknownEffect(schema)(body).pipe(Effect.mapError(() => new WatchError({ message: "Invalid GitHub response" })));
|
|
29
29
|
}
|
|
30
30
|
function pages(options: Options, path: string) {
|
|
31
|
-
return
|
|
32
|
-
const bodies: unknown[] = [];
|
|
33
|
-
const visited = new Set<string>();
|
|
34
|
-
let url: string | undefined = `${options.apiUrl}/repos/${options.repository}${path}`;
|
|
35
|
-
while (url !== undefined) {
|
|
36
|
-
if (visited.has(url)) {
|
|
37
|
-
return yield* Effect.fail(new WatchError({ message: "Repeated pagination URL" }));
|
|
38
|
-
}
|
|
39
|
-
visited.add(url);
|
|
40
|
-
const response: Effect.Success<ReturnType<typeof http.get>> = yield* http.get(url, options.token);
|
|
41
|
-
bodies.push(response.body);
|
|
42
|
-
url = response.next;
|
|
43
|
-
}
|
|
44
|
-
return bodies;
|
|
45
|
-
});
|
|
31
|
+
return http.list(`${options.apiUrl}/repos/${options.repository}${path}`);
|
|
46
32
|
}
|
|
47
33
|
return {
|
|
48
34
|
confirmQueue: (options: Options, snapshot: Snapshot) => confirmQueue(http, options, snapshot),
|
|
@@ -66,8 +52,8 @@ export const GitHub = defineService({
|
|
|
66
52
|
}),
|
|
67
53
|
snapshot: (options: Options, number: number, queued: ReadonlySet<number>) =>
|
|
68
54
|
Effect.gen(function* () {
|
|
69
|
-
const response = yield* http.get(`${options.apiUrl}/repos/${options.repository}/pulls/${number}
|
|
70
|
-
const pr = yield* decode(Pull, response
|
|
55
|
+
const response = yield* http.get(`${options.apiUrl}/repos/${options.repository}/pulls/${number}`);
|
|
56
|
+
const pr = yield* decode(Pull, response);
|
|
71
57
|
if (pr.number !== number) {
|
|
72
58
|
return yield* Effect.fail(new WatchError({ message: "GitHub returned the wrong PR" }));
|
|
73
59
|
}
|
|
@@ -78,8 +64,8 @@ export const GitHub = defineService({
|
|
|
78
64
|
if (pr.state === "open") {
|
|
79
65
|
const bodies = yield* pages(options, `/commits/${pr.head.sha}/check-runs?per_page=100&filter=latest`);
|
|
80
66
|
const checks = yield* Effect.forEach(bodies, (body) => decode(Checks, body));
|
|
81
|
-
const statusResponse = yield* http.get(`${options.apiUrl}/repos/${options.repository}/commits/${pr.head.sha}/status
|
|
82
|
-
const status = yield* decode(Status, statusResponse
|
|
67
|
+
const statusResponse = yield* http.get(`${options.apiUrl}/repos/${options.repository}/commits/${pr.head.sha}/status`);
|
|
68
|
+
const status = yield* decode(Status, statusResponse);
|
|
83
69
|
failed =
|
|
84
70
|
checks.some((page) => page.checkRuns.some((check) => check.status === "completed" && failures.has(check.conclusion ?? "")))
|
|
85
71
|
|| ["failure", "error"].includes(status.state);
|
package/src/GitHubHttp.ts
CHANGED
|
@@ -1,55 +1,71 @@
|
|
|
1
|
-
import { Clock, Effect } from "effect";
|
|
2
|
-
import {
|
|
1
|
+
import { Clock, DateTime, Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { GitHubRateLimited } from "@shivaedev/effect-github/errors.ts";
|
|
3
|
+
import { GitHub } from "@shivaedev/effect-github/GitHub.ts";
|
|
3
4
|
import { defineService } from "@shivaedev/effect-service/defineService.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import { WatchError } from "#model.ts";
|
|
6
|
+
import { pollDelay, quotaRetryAt } from "#quota.ts";
|
|
7
|
+
|
|
7
8
|
export const GitHubHttp = defineService({
|
|
8
9
|
id: "@shivaedev/pr-watch/GitHubHttp",
|
|
9
10
|
initialize: Effect.gen(function* () {
|
|
10
|
-
return {
|
|
11
|
-
budget: { blockedUntil: 0, limit: 5000, remaining: 5000, reset: 0 } satisfies Budget,
|
|
12
|
-
cache: new Map<string, CachedResponse>(),
|
|
13
|
-
client: yield* HttpClient.HttpClient,
|
|
14
|
-
requests: 0,
|
|
15
|
-
};
|
|
11
|
+
return { github: yield* GitHub, requests: 0, retryAt: 0 };
|
|
16
12
|
}),
|
|
17
13
|
methods: (state) => {
|
|
18
|
-
function request
|
|
14
|
+
function request<T, E, R>(operation: Effect.Effect<T, E, R>) {
|
|
19
15
|
return Effect.gen(function* () {
|
|
20
16
|
const now = yield* Clock.currentTimeMillis;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const cached = body === undefined ? state.cache.get(url) : undefined;
|
|
25
|
-
state.requests += 1;
|
|
26
|
-
const response = yield* fetchResponse(state.client, url, token, cached, body);
|
|
27
|
-
observeBudget(state.budget, response, yield* Clock.currentTimeMillis);
|
|
28
|
-
if (response.status === 304 && cached !== undefined) {
|
|
29
|
-
return cached;
|
|
17
|
+
const observations = yield* state.github.observations();
|
|
18
|
+
if (quotaRetryAt(observations, state.retryAt) > now) {
|
|
19
|
+
return yield* new WatchError({ message: "GitHub quota cooldown", retryAt: quotaRetryAt(observations, state.retryAt) });
|
|
30
20
|
}
|
|
31
|
-
|
|
32
|
-
if (body === undefined) {
|
|
33
|
-
state.cache.set(url, result);
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
21
|
+
return yield* operation;
|
|
36
22
|
}).pipe(
|
|
37
23
|
Effect.scoped,
|
|
38
24
|
Effect.timeout("30 seconds"),
|
|
39
|
-
Effect.
|
|
25
|
+
Effect.tapError((cause) =>
|
|
26
|
+
Effect.sync(() => {
|
|
27
|
+
if (cause instanceof GitHubRateLimited) {
|
|
28
|
+
state.retryAt = DateTime.toEpochMillis(cause.retryAt);
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
Effect.mapError((cause) => {
|
|
33
|
+
if (cause instanceof WatchError) {
|
|
34
|
+
return cause;
|
|
35
|
+
}
|
|
36
|
+
if (cause instanceof GitHubRateLimited) {
|
|
37
|
+
return new WatchError({ message: "GitHub quota cooldown", retryAt: DateTime.toEpochMillis(cause.retryAt) });
|
|
38
|
+
}
|
|
39
|
+
return new WatchError({ message: "GitHub request failed" });
|
|
40
|
+
}),
|
|
40
41
|
);
|
|
41
42
|
}
|
|
43
|
+
function counted<T, E, R>(operation: Effect.Effect<T, E, R>) {
|
|
44
|
+
return Effect.suspend(() => {
|
|
45
|
+
state.requests += 1;
|
|
46
|
+
return operation;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
42
49
|
return {
|
|
43
50
|
delay: (interval: number) =>
|
|
44
51
|
Effect.gen(function* () {
|
|
45
52
|
const now = yield* Clock.currentTimeMillis;
|
|
46
|
-
const
|
|
53
|
+
const observations = yield* state.github.observations();
|
|
54
|
+
const delay = pollDelay(observations, interval, Math.max(1, state.requests), now, state.retryAt);
|
|
47
55
|
state.requests = 0;
|
|
48
56
|
return delay;
|
|
49
57
|
}),
|
|
50
|
-
get: (url: string
|
|
51
|
-
|
|
58
|
+
get: (url: string) => request(counted(state.github.read(url)).pipe(Effect.flatMap((response) => response.decode(Schema.Unknown)))),
|
|
59
|
+
list: (url: string) =>
|
|
60
|
+
request(
|
|
61
|
+
Stream.unwrap(state.github.list(url)).pipe(
|
|
62
|
+
Stream.mapEffect((response) => counted(response.decode(Schema.Unknown))),
|
|
63
|
+
Stream.runCollect,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
post: (url: string, body: unknown) =>
|
|
67
|
+
request(counted(state.github.write("POST", url, body)).pipe(Effect.flatMap((response) => response.decode(Schema.Unknown)))),
|
|
52
68
|
};
|
|
53
69
|
},
|
|
54
|
-
requires: [
|
|
70
|
+
requires: [GitHub],
|
|
55
71
|
});
|
package/src/Watcher.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { defineService } from "@shivaedev/effect-service/defineService.ts";
|
|
3
|
-
import { events } from "
|
|
4
|
-
import { GitHub } from "
|
|
5
|
-
import type { Options, Snapshot, State, WatchError } from "
|
|
6
|
-
import { StateFile } from "
|
|
3
|
+
import { events } from "#events.ts";
|
|
4
|
+
import { GitHub } from "#GitHub.ts";
|
|
5
|
+
import type { Options, Snapshot, State, WatchError } from "#model.ts";
|
|
6
|
+
import { StateFile } from "#StateFile.ts";
|
|
7
7
|
|
|
8
8
|
function activeNumbers(options: Options, state: State) {
|
|
9
9
|
return [...new Set([...options.numbers, ...(state.openNumbers ?? []), ...Object.keys(state.prs).map(Number)])].filter((number) => {
|
|
@@ -25,7 +25,7 @@ export const Watcher = defineService({
|
|
|
25
25
|
before === undefined && snapshot.state === "closed" && state.openNumbers?.includes(snapshot.number)
|
|
26
26
|
? { ...snapshot, merged: false, state: "open" as const }
|
|
27
27
|
: before;
|
|
28
|
-
for (const line of events(baseline, snapshot)) {
|
|
28
|
+
for (const line of events(baseline, snapshot, options.events)) {
|
|
29
29
|
yield* emit(line);
|
|
30
30
|
}
|
|
31
31
|
const next = {
|
package/src/arguments.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
import { type
|
|
3
|
+
import { type EventKind, eventKinds } from "#events.ts";
|
|
4
|
+
import { type Options, WatchError } from "#model.ts";
|
|
4
5
|
export const help = `pr-watch --repo OWNER/REPO [--fleet | PR_NUMBER ...]
|
|
5
6
|
--state PATH Snapshot file (default .pr-watch/OWNER-REPO.json)
|
|
6
7
|
--interval SECONDS Poll interval (default 60; slows as quota falls)
|
|
8
|
+
--events KINDS Comma-separated PR event kinds (health lines always print)
|
|
7
9
|
--once Poll once and exit
|
|
8
10
|
--api-url URL GitHub REST base URL (default https://api.github.com)
|
|
9
11
|
--help Show this help
|
|
10
12
|
Authentication: GH_TOKEN or GITHUB_TOKEN`;
|
|
11
|
-
export function argumentsFor(args: readonly string[]
|
|
13
|
+
export function argumentsFor(args: readonly string[]) {
|
|
12
14
|
return Effect.try({
|
|
13
15
|
catch: (cause) => new WatchError({ message: cause instanceof Error ? cause.message : "Invalid arguments" }),
|
|
14
16
|
try: () => {
|
|
@@ -17,6 +19,7 @@ export function argumentsFor(args: readonly string[], token: string) {
|
|
|
17
19
|
args: [...args],
|
|
18
20
|
options: {
|
|
19
21
|
"api-url": { type: "string" },
|
|
22
|
+
events: { type: "string" },
|
|
20
23
|
fleet: { type: "boolean" },
|
|
21
24
|
help: { type: "boolean" },
|
|
22
25
|
interval: { type: "string" },
|
|
@@ -32,6 +35,7 @@ export function argumentsFor(args: readonly string[], token: string) {
|
|
|
32
35
|
if (repository.match(/^[\w.-]+\/[\w.-]+$/u) === null) {
|
|
33
36
|
throw new Error("--repo OWNER/REPO is required");
|
|
34
37
|
}
|
|
38
|
+
const selected = eventsFor(values.events);
|
|
35
39
|
const numbers = numbersFor(positionals, values.fleet ?? false);
|
|
36
40
|
const interval = intervalFor(values.interval);
|
|
37
41
|
const apiUrl = apiUrlFor(values["api-url"]);
|
|
@@ -40,12 +44,12 @@ export function argumentsFor(args: readonly string[], token: string) {
|
|
|
40
44
|
once: values.once ?? false,
|
|
41
45
|
options: {
|
|
42
46
|
apiUrl,
|
|
47
|
+
...(selected === undefined ? {} : { events: selected }),
|
|
43
48
|
fleet: values.fleet ?? false,
|
|
44
49
|
interval,
|
|
45
50
|
numbers,
|
|
46
51
|
repository,
|
|
47
52
|
stateFile: values.state ?? `.pr-watch/${repository.replace("/", "-")}.json`,
|
|
48
|
-
token,
|
|
49
53
|
} satisfies Options,
|
|
50
54
|
};
|
|
51
55
|
},
|
|
@@ -81,3 +85,16 @@ function apiUrlFor(value: string | undefined): string {
|
|
|
81
85
|
}
|
|
82
86
|
return apiUrl;
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
function eventsFor(value: string | undefined): readonly EventKind[] | undefined {
|
|
90
|
+
if (value === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return value.split(",").map((name) => {
|
|
94
|
+
const kind = eventKinds.find((valid) => valid === name);
|
|
95
|
+
if (kind === undefined) {
|
|
96
|
+
throw new Error(`Unknown event kind ${JSON.stringify(name)}. Valid kinds: ${eventKinds.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
return kind;
|
|
99
|
+
});
|
|
100
|
+
}
|