@markjaquith/agency 3.2.11 → 3.2.12
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/package.json
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
import type { PullRequestRecord } from "../workbase/schemas"
|
|
15
15
|
import {
|
|
16
16
|
normalizePullRequestRecord,
|
|
17
|
+
parseGitHubPullRequestList,
|
|
17
18
|
parsePullRequestRecord,
|
|
18
|
-
recordFromGitHubJson,
|
|
19
19
|
recordFromGitHubUrl,
|
|
20
20
|
repositoryFromRemote,
|
|
21
21
|
resolveDeliveryCommand,
|
|
@@ -236,17 +236,7 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
236
236
|
})
|
|
237
237
|
}
|
|
238
238
|
const records = yield* Effect.try({
|
|
239
|
-
try: () =>
|
|
240
|
-
const parsed: unknown = JSON.parse(listed.stdout)
|
|
241
|
-
if (!Array.isArray(parsed)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
"GitHub CLI did not return a pull request list",
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
return parsed.map((value) =>
|
|
247
|
-
recordFromGitHubJson(value as Record<string, unknown>),
|
|
248
|
-
)
|
|
249
|
-
},
|
|
239
|
+
try: () => parseGitHubPullRequestList(listed.stdout),
|
|
250
240
|
catch: (cause) =>
|
|
251
241
|
new PullRequestError({
|
|
252
242
|
message:
|
|
@@ -613,6 +613,89 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
613
613
|
).toBe(false)
|
|
614
614
|
})
|
|
615
615
|
|
|
616
|
+
test("reports malformed successful GitHub responses", async () => {
|
|
617
|
+
await runTestEffect(
|
|
618
|
+
TaskService.pipe(
|
|
619
|
+
Effect.flatMap((service) =>
|
|
620
|
+
service.create(
|
|
621
|
+
{
|
|
622
|
+
id: "malformed-detail",
|
|
623
|
+
ticketUrl: null,
|
|
624
|
+
repo: "agency",
|
|
625
|
+
branch: "feat/example",
|
|
626
|
+
base: "main",
|
|
627
|
+
},
|
|
628
|
+
root,
|
|
629
|
+
),
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
await runTestEffect(
|
|
634
|
+
PullRequestService.pipe(
|
|
635
|
+
Effect.flatMap((service) =>
|
|
636
|
+
service.setUrl(
|
|
637
|
+
"malformed-detail",
|
|
638
|
+
undefined,
|
|
639
|
+
"https://github.com/example/agency/pull/42",
|
|
640
|
+
root,
|
|
641
|
+
),
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
)
|
|
645
|
+
await runTestEffect(
|
|
646
|
+
TaskService.pipe(
|
|
647
|
+
Effect.flatMap((service) =>
|
|
648
|
+
service.create(
|
|
649
|
+
{
|
|
650
|
+
id: "malformed-list",
|
|
651
|
+
ticketUrl: null,
|
|
652
|
+
repo: "agency",
|
|
653
|
+
branch: "feat/malformed-list",
|
|
654
|
+
base: "main",
|
|
655
|
+
},
|
|
656
|
+
root,
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
await Bun.write(join(root, "bin", "gh"), "#!/bin/sh\nprintf '{}\\n'\n")
|
|
662
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
663
|
+
|
|
664
|
+
const result = await runTestEffect(
|
|
665
|
+
SyncService.pipe(
|
|
666
|
+
Effect.flatMap((service) =>
|
|
667
|
+
service.reconcile({ cwd: root, taskId: "malformed-detail" }),
|
|
668
|
+
),
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
expect(result.warnings).toContainEqual({
|
|
672
|
+
kind: "pr-provider-invalid-output",
|
|
673
|
+
target: "task:malformed-detail",
|
|
674
|
+
message: expect.stringContaining(
|
|
675
|
+
"GitHub CLI did not return a valid pull request",
|
|
676
|
+
),
|
|
677
|
+
})
|
|
678
|
+
expect(result.executions[0]?.pr).toMatchObject({
|
|
679
|
+
url: "https://github.com/example/agency/pull/42",
|
|
680
|
+
state: "open",
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
const listResult = await runTestEffect(
|
|
684
|
+
SyncService.pipe(
|
|
685
|
+
Effect.flatMap((service) =>
|
|
686
|
+
service.reconcile({ cwd: root, taskId: "malformed-list" }),
|
|
687
|
+
),
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
expect(listResult.warnings).toContainEqual({
|
|
691
|
+
kind: "pr-provider-invalid-output",
|
|
692
|
+
target: "task:malformed-list",
|
|
693
|
+
message: expect.stringContaining(
|
|
694
|
+
"GitHub CLI did not return a valid pull request list",
|
|
695
|
+
),
|
|
696
|
+
})
|
|
697
|
+
})
|
|
698
|
+
|
|
616
699
|
test("reconciles a uniquely discovered merged PR without materializing", async () => {
|
|
617
700
|
await runTestEffect(
|
|
618
701
|
TaskService.pipe(
|
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
} from "../workbase/frontmatter"
|
|
15
15
|
import {
|
|
16
16
|
normalizePullRequestRecord,
|
|
17
|
+
parseGitHubPullRequest,
|
|
18
|
+
parseGitHubPullRequestList,
|
|
17
19
|
parseOptionalPullRequestRecord,
|
|
18
|
-
recordFromGitHubJson,
|
|
19
20
|
resolveDeliveryCommand,
|
|
20
21
|
} from "../workbase/delivery-command"
|
|
21
22
|
import { FileSystemService } from "./FileSystemService"
|
|
@@ -135,14 +136,6 @@ const parseWorktrees = (output: string): RegisteredWorktree[] => {
|
|
|
135
136
|
return worktrees
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
const parseJson = <T>(value: string, fallback: T): T => {
|
|
139
|
-
try {
|
|
140
|
-
return JSON.parse(value) as T
|
|
141
|
-
} catch {
|
|
142
|
-
return fallback
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
139
|
const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
147
140
|
|
|
148
141
|
const originRef = (ref: string) =>
|
|
@@ -172,17 +165,18 @@ interface PullRequestQuery {
|
|
|
172
165
|
const mergedPullRequestFromGitHub = (
|
|
173
166
|
data: ExecutionData,
|
|
174
167
|
query: PullRequestQuery | undefined,
|
|
168
|
+
details: readonly PullRequestRecord[] | undefined,
|
|
175
169
|
) => {
|
|
176
|
-
if (!query?.result || query.result.exitCode !== 0) return null
|
|
170
|
+
if (!query?.result || query.result.exitCode !== 0 || !details) return null
|
|
177
171
|
const existing = data.pr ? normalizePullRequestRecord(data.pr) : null
|
|
178
|
-
const
|
|
179
|
-
?
|
|
180
|
-
:
|
|
172
|
+
const matches = existing
|
|
173
|
+
? details
|
|
174
|
+
: details.filter(
|
|
181
175
|
(item) =>
|
|
182
|
-
item.
|
|
176
|
+
item.headBranch === data.branch && item.baseBranch === data.base,
|
|
183
177
|
)
|
|
184
|
-
if (
|
|
185
|
-
const current =
|
|
178
|
+
if (matches.length !== 1) return null
|
|
179
|
+
const current = matches[0]!
|
|
186
180
|
if (
|
|
187
181
|
current.merged !== true ||
|
|
188
182
|
current.headRepository?.toLowerCase() !==
|
|
@@ -493,6 +487,37 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
493
487
|
{ concurrency: 8 },
|
|
494
488
|
),
|
|
495
489
|
)
|
|
490
|
+
const githubResponses = new Map<
|
|
491
|
+
string,
|
|
492
|
+
| {
|
|
493
|
+
readonly ok: true
|
|
494
|
+
readonly details: readonly PullRequestRecord[]
|
|
495
|
+
}
|
|
496
|
+
| { readonly ok: false; readonly message: string }
|
|
497
|
+
>()
|
|
498
|
+
if (!config.delivery) {
|
|
499
|
+
for (const record of queryRecords) {
|
|
500
|
+
const query = prQueries.get(record.key)
|
|
501
|
+
if (query?.result?.exitCode !== 0) continue
|
|
502
|
+
try {
|
|
503
|
+
githubResponses.set(record.key, {
|
|
504
|
+
ok: true,
|
|
505
|
+
details: record.data.pr
|
|
506
|
+
? [parseGitHubPullRequest(query.result.stdout)]
|
|
507
|
+
: parseGitHubPullRequestList(query.result.stdout),
|
|
508
|
+
})
|
|
509
|
+
} catch (cause) {
|
|
510
|
+
const message =
|
|
511
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
512
|
+
githubResponses.set(record.key, { ok: false, message })
|
|
513
|
+
warnings.push({
|
|
514
|
+
kind: "pr-provider-invalid-output",
|
|
515
|
+
target: record.key,
|
|
516
|
+
message,
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
496
521
|
const reviewSourceQueries = new Map(
|
|
497
522
|
yield* Effect.forEach(
|
|
498
523
|
reviewRecords,
|
|
@@ -534,11 +559,13 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
534
559
|
}
|
|
535
560
|
const checkoutRecords = records.filter((record) => {
|
|
536
561
|
if (record.data.completion) return false
|
|
562
|
+
const githubResponse = githubResponses.get(record.key)
|
|
537
563
|
const merged = config.delivery
|
|
538
564
|
? null
|
|
539
565
|
: mergedPullRequestFromGitHub(
|
|
540
566
|
record.data,
|
|
541
567
|
prQueries.get(record.key),
|
|
568
|
+
githubResponse?.ok ? githubResponse.details : undefined,
|
|
542
569
|
)
|
|
543
570
|
return merged === null
|
|
544
571
|
})
|
|
@@ -590,9 +617,13 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
590
617
|
const codePath = join(dirname(record.path), "code")
|
|
591
618
|
const checkoutStates: CheckoutState[] = []
|
|
592
619
|
const query = prQueries.get(record.key)
|
|
620
|
+
const githubResponse = githubResponses.get(record.key)
|
|
621
|
+
const githubDetails = githubResponse?.ok
|
|
622
|
+
? githubResponse.details
|
|
623
|
+
: undefined
|
|
593
624
|
const remoteMergedPr = config.delivery
|
|
594
625
|
? null
|
|
595
|
-
: mergedPullRequestFromGitHub(data, query)
|
|
626
|
+
: mergedPullRequestFromGitHub(data, query, githubDetails)
|
|
596
627
|
const skipCheckoutReconciliation =
|
|
597
628
|
Boolean(data.completion) || remoteMergedPr !== null
|
|
598
629
|
let materialize = false
|
|
@@ -947,27 +978,25 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
947
978
|
} else if (existing) {
|
|
948
979
|
const viewed = query.result!
|
|
949
980
|
if (viewed.exitCode === 0) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
action: "Correct the declaration or recorded PR URL",
|
|
970
|
-
})
|
|
981
|
+
if (githubDetails) {
|
|
982
|
+
current = githubDetails[0]!
|
|
983
|
+
pr = current
|
|
984
|
+
if (
|
|
985
|
+
current.headRepository?.toLowerCase() !==
|
|
986
|
+
remoteRepository.toLowerCase() ||
|
|
987
|
+
current.headBranch !== data.branch ||
|
|
988
|
+
current.baseRepository?.toLowerCase() !==
|
|
989
|
+
current.repository.toLowerCase() ||
|
|
990
|
+
current.baseBranch !== data.base
|
|
991
|
+
) {
|
|
992
|
+
prConflict = true
|
|
993
|
+
unresolved.push({
|
|
994
|
+
kind: "pr-repository-conflict",
|
|
995
|
+
target: record.key,
|
|
996
|
+
message: `Recorded PR head does not match '${remoteRepository}:${data.branch}' or base '${current.repository}:${data.base}'`,
|
|
997
|
+
action: "Correct the declaration or recorded PR URL",
|
|
998
|
+
})
|
|
999
|
+
}
|
|
971
1000
|
}
|
|
972
1001
|
} else {
|
|
973
1002
|
pr = { url: existing.url, state: "unavailable" }
|
|
@@ -980,17 +1009,14 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
980
1009
|
} else {
|
|
981
1010
|
const listed = query.result!
|
|
982
1011
|
if (listed.exitCode === 0) {
|
|
983
|
-
const matches =
|
|
984
|
-
listed.stdout,
|
|
985
|
-
[],
|
|
986
|
-
).filter(
|
|
1012
|
+
const matches = (githubDetails ?? []).filter(
|
|
987
1013
|
(item) =>
|
|
988
|
-
item.
|
|
989
|
-
item.
|
|
1014
|
+
item.headBranch === data.branch &&
|
|
1015
|
+
item.baseBranch === data.base,
|
|
990
1016
|
)
|
|
991
1017
|
if (matches.length === 1) {
|
|
992
|
-
current =
|
|
993
|
-
pr =
|
|
1018
|
+
current = matches[0]!
|
|
1019
|
+
pr = current
|
|
994
1020
|
if (
|
|
995
1021
|
current.headRepository?.toLowerCase() !==
|
|
996
1022
|
remoteRepository.toLowerCase() ||
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
2
|
import {
|
|
3
|
+
parseGitHubPullRequest,
|
|
4
|
+
parseGitHubPullRequestList,
|
|
3
5
|
parsePullRequestRecord,
|
|
4
|
-
recordFromGitHubJson,
|
|
5
6
|
resolveDeliveryCommand,
|
|
6
7
|
resolveGitHubCreateCommand,
|
|
7
8
|
validateDelivery,
|
|
@@ -116,18 +117,24 @@ describe("delivery commands", () => {
|
|
|
116
117
|
const base = {
|
|
117
118
|
number: 17,
|
|
118
119
|
url: "https://github.com/example/agency/pull/17",
|
|
120
|
+
title: "Ship",
|
|
119
121
|
isDraft: false,
|
|
122
|
+
headRefName: "feat/example",
|
|
123
|
+
baseRefName: "main",
|
|
124
|
+
headRepository: { nameWithOwner: "fork/agency" },
|
|
125
|
+
mergedAt: null,
|
|
126
|
+
mergeCommit: null,
|
|
127
|
+
mergeable: "UNKNOWN",
|
|
120
128
|
}
|
|
121
129
|
expect(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}),
|
|
130
|
+
parseGitHubPullRequest(
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
...base,
|
|
133
|
+
state: "OPEN",
|
|
134
|
+
baseRepository: { nameWithOwner: "example/agency" },
|
|
135
|
+
mergeable: "MERGEABLE",
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
131
138
|
).toMatchObject({
|
|
132
139
|
state: "open",
|
|
133
140
|
merged: false,
|
|
@@ -138,19 +145,36 @@ describe("delivery commands", () => {
|
|
|
138
145
|
mergeable: true,
|
|
139
146
|
})
|
|
140
147
|
expect(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
parseGitHubPullRequest(
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
...base,
|
|
151
|
+
state: "OPEN",
|
|
152
|
+
mergeable: "CONFLICTING",
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
146
155
|
).toMatchObject({ state: "open", merged: false, mergeable: false })
|
|
147
156
|
expect(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
parseGitHubPullRequest(
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
...base,
|
|
160
|
+
state: "CLOSED",
|
|
161
|
+
mergedAt: "2026-07-21T00:00:00Z",
|
|
162
|
+
mergeCommit: { oid: "abc" },
|
|
163
|
+
mergeable: "UNKNOWN",
|
|
164
|
+
}),
|
|
165
|
+
),
|
|
154
166
|
).toMatchObject({ state: "merged", merged: true, mergeable: null })
|
|
155
167
|
})
|
|
168
|
+
|
|
169
|
+
test("rejects malformed GitHub detail and list responses", () => {
|
|
170
|
+
expect(() => parseGitHubPullRequest("not-json")).toThrow(
|
|
171
|
+
"GitHub CLI did not return valid JSON for pull request",
|
|
172
|
+
)
|
|
173
|
+
expect(() => parseGitHubPullRequest("{}")).toThrow(
|
|
174
|
+
"GitHub CLI did not return a valid pull request",
|
|
175
|
+
)
|
|
176
|
+
expect(() => parseGitHubPullRequestList("{}")).toThrow(
|
|
177
|
+
"GitHub CLI did not return a valid pull request list",
|
|
178
|
+
)
|
|
179
|
+
})
|
|
156
180
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "@effect/schema"
|
|
1
|
+
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
2
|
import type { PullRequestRecord, WorkbaseConfig } from "./schemas"
|
|
3
3
|
import { PullRequestRecord as PullRequestRecordSchema } from "./schemas"
|
|
4
4
|
|
|
@@ -155,9 +155,47 @@ export const recordFromGitHubUrl = (url: string): PullRequestRecord => {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
const GitHubRepository = Schema.Struct({
|
|
159
|
+
nameWithOwner: Schema.String.pipe(Schema.minLength(1)),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const GitHubPullRequest = Schema.Struct({
|
|
163
|
+
number: Schema.Number,
|
|
164
|
+
state: Schema.Literal("OPEN", "CLOSED", "MERGED"),
|
|
165
|
+
title: Schema.optional(Schema.String),
|
|
166
|
+
isDraft: Schema.Boolean,
|
|
167
|
+
headRefName: Schema.String,
|
|
168
|
+
baseRefName: Schema.String,
|
|
169
|
+
headRepository: Schema.NullOr(GitHubRepository),
|
|
170
|
+
baseRepository: Schema.optional(Schema.NullOr(GitHubRepository)),
|
|
171
|
+
url: Schema.String,
|
|
172
|
+
mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
|
|
173
|
+
mergeCommit: Schema.optional(
|
|
174
|
+
Schema.NullOr(Schema.Struct({ oid: Schema.String })),
|
|
175
|
+
),
|
|
176
|
+
mergeable: Schema.Literal("MERGEABLE", "CONFLICTING", "UNKNOWN"),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const decodeGitHubPullRequest = Schema.decodeUnknownEither(GitHubPullRequest)
|
|
180
|
+
const decodeGitHubPullRequests = Schema.decodeUnknownEither(
|
|
181
|
+
Schema.Array(GitHubPullRequest),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const invalidGitHubResponse = (
|
|
185
|
+
kind: "pull request" | "pull request list",
|
|
186
|
+
error: Parameters<typeof TreeFormatter.formatErrorSync>[0],
|
|
187
|
+
) =>
|
|
188
|
+
new Error(
|
|
189
|
+
`GitHub CLI did not return a valid ${kind}: ${TreeFormatter.formatErrorSync(error)}`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const recordFromGitHubJson = (value: unknown): PullRequestRecord => {
|
|
193
|
+
const decoded = decodeGitHubPullRequest(value)
|
|
194
|
+
if (decoded._tag === "Left") {
|
|
195
|
+
throw invalidGitHubResponse("pull request", decoded.left)
|
|
196
|
+
}
|
|
197
|
+
const detail = decoded.right
|
|
198
|
+
const record = recordFromGitHubUrl(detail.url)
|
|
161
199
|
const repositoryName = (repository: unknown) => {
|
|
162
200
|
if (!repository || typeof repository !== "object") return undefined
|
|
163
201
|
const nameWithOwner = (repository as Record<string, unknown>).nameWithOwner
|
|
@@ -165,19 +203,17 @@ export const recordFromGitHubJson = (value: Record<string, unknown>) => {
|
|
|
165
203
|
? nameWithOwner
|
|
166
204
|
: undefined
|
|
167
205
|
}
|
|
168
|
-
const githubState =
|
|
169
|
-
const merged = githubState === "merged" ||
|
|
170
|
-
const mergeable =
|
|
206
|
+
const githubState = detail.state.toLowerCase()
|
|
207
|
+
const merged = githubState === "merged" || detail.mergedAt != null
|
|
208
|
+
const mergeable = detail.mergeable.toLowerCase()
|
|
171
209
|
return {
|
|
172
210
|
...record,
|
|
173
|
-
headRepository: repositoryName(
|
|
174
|
-
headBranch:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
baseBranch:
|
|
178
|
-
typeof value.baseRefName === "string" ? value.baseRefName : undefined,
|
|
211
|
+
headRepository: repositoryName(detail.headRepository),
|
|
212
|
+
headBranch: detail.headRefName,
|
|
213
|
+
baseRepository: repositoryName(detail.baseRepository) ?? record.repository,
|
|
214
|
+
baseBranch: detail.baseRefName,
|
|
179
215
|
state: merged ? "merged" : githubState === "closed" ? "closed" : "open",
|
|
180
|
-
draft:
|
|
216
|
+
draft: detail.isDraft,
|
|
181
217
|
merged,
|
|
182
218
|
mergeable:
|
|
183
219
|
mergeable === "mergeable"
|
|
@@ -188,6 +224,32 @@ export const recordFromGitHubJson = (value: Record<string, unknown>) => {
|
|
|
188
224
|
} satisfies PullRequestRecord
|
|
189
225
|
}
|
|
190
226
|
|
|
227
|
+
const parseJson = (
|
|
228
|
+
value: string,
|
|
229
|
+
kind: "pull request" | "pull request list",
|
|
230
|
+
) => {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(value) as unknown
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error(`GitHub CLI did not return valid JSON for ${kind}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const parseGitHubPullRequest = (value: string): PullRequestRecord =>
|
|
239
|
+
recordFromGitHubJson(parseJson(value, "pull request"))
|
|
240
|
+
|
|
241
|
+
export const parseGitHubPullRequestList = (
|
|
242
|
+
value: string,
|
|
243
|
+
): readonly PullRequestRecord[] => {
|
|
244
|
+
const decoded = decodeGitHubPullRequests(
|
|
245
|
+
parseJson(value, "pull request list"),
|
|
246
|
+
)
|
|
247
|
+
if (decoded._tag === "Left") {
|
|
248
|
+
throw invalidGitHubResponse("pull request list", decoded.left)
|
|
249
|
+
}
|
|
250
|
+
return decoded.right.map(recordFromGitHubJson)
|
|
251
|
+
}
|
|
252
|
+
|
|
191
253
|
export const normalizePullRequestRecord = (
|
|
192
254
|
record: PullRequestRecord | string,
|
|
193
255
|
): PullRequestRecord =>
|