@markjaquith/agency 2.61.0 → 2.61.2
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 +1 -1
- package/src/cli.test.ts +10 -1
- package/src/commands/sync.test.ts +39 -1
- package/src/commands/sync.ts +29 -4
- package/src/services/PullRequestService.ts +11 -2
- package/src/services/SyncService.test.ts +134 -3
- package/src/services/SyncService.ts +45 -26
- package/src/workbase/delivery-command.test.ts +18 -2
- package/src/workbase/delivery-command.ts +13 -0
- package/src/workbase/schemas.ts +4 -0
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
setDefaultTimeout,
|
|
7
|
+
test,
|
|
8
|
+
} from "bun:test"
|
|
2
9
|
import { access, chmod, mkdir, realpath, stat, symlink } from "node:fs/promises"
|
|
3
10
|
import { join, sep } from "node:path"
|
|
4
11
|
import errorFixture from "../fixtures/protocol/error.json"
|
|
@@ -9,6 +16,8 @@ const projectRoot = join(import.meta.dir, "..")
|
|
|
9
16
|
const cliPath = join(projectRoot, "cli.ts")
|
|
10
17
|
const isolatedConfigHome = await createTempDir()
|
|
11
18
|
|
|
19
|
+
setDefaultTimeout(15_000)
|
|
20
|
+
|
|
12
21
|
afterAll(() => cleanupTempDir(isolatedConfigHome))
|
|
13
22
|
|
|
14
23
|
interface CliResult {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { chmod, mkdir } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { TaskService } from "../services/TaskService"
|
|
6
6
|
import {
|
|
@@ -99,6 +99,44 @@ describe("sync command", () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
test("groups repeated human-readable warnings by affected target", async () => {
|
|
103
|
+
await runTestEffect(
|
|
104
|
+
TaskService.pipe(
|
|
105
|
+
Effect.flatMap((service) =>
|
|
106
|
+
service.create(
|
|
107
|
+
{
|
|
108
|
+
id: "second",
|
|
109
|
+
ticketUrl: null,
|
|
110
|
+
repo: "agency",
|
|
111
|
+
branch: "task/second",
|
|
112
|
+
base: "main",
|
|
113
|
+
},
|
|
114
|
+
root,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
const originalPath = process.env.PATH
|
|
120
|
+
const bin = join(root, "bin")
|
|
121
|
+
await mkdir(bin)
|
|
122
|
+
await Bun.write(
|
|
123
|
+
join(bin, "gh"),
|
|
124
|
+
'#!/bin/sh\necho "provider unavailable" >&2\nexit 1\n',
|
|
125
|
+
)
|
|
126
|
+
await chmod(join(bin, "gh"), 0o755)
|
|
127
|
+
process.env.PATH = `${bin}:${originalPath}`
|
|
128
|
+
try {
|
|
129
|
+
const logs = await captureLogs(() =>
|
|
130
|
+
runTestEffect(sync({ cwd: root, dryRun: true })),
|
|
131
|
+
)
|
|
132
|
+
expect(
|
|
133
|
+
logs.filter((line) => line.includes("provider unavailable")),
|
|
134
|
+
).toEqual(["Warning 'task:example', 'task:second': provider unavailable"])
|
|
135
|
+
} finally {
|
|
136
|
+
process.env.PATH = originalPath
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
102
140
|
test("reports human-readable progress without polluting JSON output", async () => {
|
|
103
141
|
const updates: string[] = []
|
|
104
142
|
const progress: Progress = {
|
package/src/commands/sync.ts
CHANGED
|
@@ -8,6 +8,27 @@ interface SyncCommandOptions extends BaseCommandOptions {
|
|
|
8
8
|
readonly dryRun?: boolean
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface Notice {
|
|
12
|
+
readonly kind: string
|
|
13
|
+
readonly target: string
|
|
14
|
+
readonly message: string
|
|
15
|
+
readonly action?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const groupedNotices = <T extends Notice>(notices: readonly T[]) => {
|
|
19
|
+
const groups = new Map<string, { notice: T; targets: string[] }>()
|
|
20
|
+
for (const notice of notices) {
|
|
21
|
+
const key = JSON.stringify([notice.kind, notice.message, notice.action])
|
|
22
|
+
const group = groups.get(key)
|
|
23
|
+
if (group) group.targets.push(notice.target)
|
|
24
|
+
else groups.set(key, { notice, targets: [notice.target] })
|
|
25
|
+
}
|
|
26
|
+
return groups.values()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const formatTargets = (targets: readonly string[]) =>
|
|
30
|
+
targets.map((target) => `'${target}'`).join(", ")
|
|
31
|
+
|
|
11
32
|
export const sync = (
|
|
12
33
|
options: SyncCommandOptions = {},
|
|
13
34
|
progress: Progress = createProgress({
|
|
@@ -62,9 +83,11 @@ export const sync = (
|
|
|
62
83
|
`${change.status === "applied" ? "Applied" : "Planned"} ${change.kind} '${change.target}': ${change.message}`,
|
|
63
84
|
)
|
|
64
85
|
}
|
|
65
|
-
for (const warning of
|
|
86
|
+
for (const { notice: warning, targets } of groupedNotices(
|
|
87
|
+
result.warnings,
|
|
88
|
+
)) {
|
|
66
89
|
log(
|
|
67
|
-
`Warning
|
|
90
|
+
`Warning ${formatTargets(targets)}: ${warning.message}${warning.action ? `. ${warning.action}` : ""}`,
|
|
68
91
|
)
|
|
69
92
|
}
|
|
70
93
|
for (const issue of result.repositories.unresolved) {
|
|
@@ -72,9 +95,11 @@ export const sync = (
|
|
|
72
95
|
`Unresolved repository '${issue.alias}': ${issue.message}. ${issue.action}`,
|
|
73
96
|
)
|
|
74
97
|
}
|
|
75
|
-
for (const issue of
|
|
98
|
+
for (const { notice: issue, targets } of groupedNotices(
|
|
99
|
+
result.unresolved,
|
|
100
|
+
)) {
|
|
76
101
|
log(
|
|
77
|
-
`Unresolved
|
|
102
|
+
`Unresolved ${formatTargets(targets)}: ${issue.message}${issue.action ? `. ${issue.action}` : ""}`,
|
|
78
103
|
)
|
|
79
104
|
}
|
|
80
105
|
if (
|
|
@@ -239,7 +239,15 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
239
239
|
})
|
|
240
240
|
if (!url)
|
|
241
241
|
throw new Error("GitHub CLI did not return a pull request URL")
|
|
242
|
-
|
|
242
|
+
const normalized = normalizePullRequestRecord(url)
|
|
243
|
+
return {
|
|
244
|
+
...normalized,
|
|
245
|
+
headRepository: repository,
|
|
246
|
+
headBranch: execution.branch,
|
|
247
|
+
baseRepository: normalized.repository,
|
|
248
|
+
baseBranch: execution.base,
|
|
249
|
+
draft,
|
|
250
|
+
}
|
|
243
251
|
},
|
|
244
252
|
catch: (cause) =>
|
|
245
253
|
new PullRequestError({
|
|
@@ -249,7 +257,8 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
249
257
|
if (
|
|
250
258
|
config.delivery &&
|
|
251
259
|
(record.provider !== config.delivery.provider ||
|
|
252
|
-
record.repository.toLowerCase() !==
|
|
260
|
+
(record.headRepository ?? record.repository).toLowerCase() !==
|
|
261
|
+
repository.toLowerCase())
|
|
253
262
|
) {
|
|
254
263
|
return yield* new PullRequestError({
|
|
255
264
|
message:
|
|
@@ -64,14 +64,17 @@ case "$*" in
|
|
|
64
64
|
*mergeable*) ;;
|
|
65
65
|
*) echo "mergeable field was not requested" >&2; exit 2 ;;
|
|
66
66
|
esac
|
|
67
|
+
case "$*" in
|
|
68
|
+
*baseRepository*) echo "unsupported baseRepository field was requested" >&2; exit 3 ;;
|
|
69
|
+
esac
|
|
67
70
|
if [ "$2" = "view" ]; then
|
|
68
71
|
cat <<'JSON'
|
|
69
|
-
{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}
|
|
72
|
+
{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}
|
|
70
73
|
JSON
|
|
71
74
|
exit 0
|
|
72
75
|
fi
|
|
73
76
|
cat <<'JSON'
|
|
74
|
-
[{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
|
|
77
|
+
[{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
|
|
75
78
|
JSON
|
|
76
79
|
`,
|
|
77
80
|
)
|
|
@@ -172,6 +175,7 @@ pr: null
|
|
|
172
175
|
)
|
|
173
176
|
const invocation = await Bun.file(capture).text()
|
|
174
177
|
expect(invocation).toContain("--repo\n")
|
|
178
|
+
expect(invocation).not.toContain("baseRepository")
|
|
175
179
|
expect(invocation).toContain("GIT_DIR=")
|
|
176
180
|
expect(invocation).toContain(".jj")
|
|
177
181
|
|
|
@@ -307,6 +311,10 @@ pr: null
|
|
|
307
311
|
state: "merged",
|
|
308
312
|
draft: false,
|
|
309
313
|
merged: true,
|
|
314
|
+
headRepository: "example/agency",
|
|
315
|
+
headBranch: "feat/example",
|
|
316
|
+
baseRepository: "example/agency",
|
|
317
|
+
baseBranch: "main",
|
|
310
318
|
mergeable: true,
|
|
311
319
|
},
|
|
312
320
|
claim: { state: "released", sessionId: "session-1" },
|
|
@@ -357,7 +365,7 @@ pr: null
|
|
|
357
365
|
join(root, "bin", "gh"),
|
|
358
366
|
`#!/bin/sh
|
|
359
367
|
cat <<'JSON'
|
|
360
|
-
{"number":42,"state":"OPEN","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":null,"mergeCommit":null,"mergeable":"CONFLICTING"}
|
|
368
|
+
{"number":42,"state":"OPEN","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"baseRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":null,"mergeCommit":null,"mergeable":"CONFLICTING"}
|
|
361
369
|
JSON
|
|
362
370
|
`,
|
|
363
371
|
)
|
|
@@ -747,6 +755,89 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
747
755
|
expect(task.data).toMatchObject({ status: "open" })
|
|
748
756
|
})
|
|
749
757
|
|
|
758
|
+
test("reconciles a merged upstream PR whose head is the writable fork", async () => {
|
|
759
|
+
await runTestEffect(
|
|
760
|
+
TaskService.pipe(
|
|
761
|
+
Effect.flatMap((service) =>
|
|
762
|
+
service.create(
|
|
763
|
+
{
|
|
764
|
+
id: "forked",
|
|
765
|
+
ticketUrl: null,
|
|
766
|
+
repo: "agency",
|
|
767
|
+
branch: "task/polish-readme",
|
|
768
|
+
base: "main",
|
|
769
|
+
},
|
|
770
|
+
root,
|
|
771
|
+
),
|
|
772
|
+
),
|
|
773
|
+
),
|
|
774
|
+
)
|
|
775
|
+
await runTestEffect(
|
|
776
|
+
WorktreeService.pipe(
|
|
777
|
+
Effect.flatMap((service) =>
|
|
778
|
+
service.materialize("forked", undefined, root),
|
|
779
|
+
),
|
|
780
|
+
),
|
|
781
|
+
)
|
|
782
|
+
await git(
|
|
783
|
+
["remote", "set-url", "origin", "git@github.com:markjaquith/Pasted.git"],
|
|
784
|
+
join(root, "repos/agency"),
|
|
785
|
+
)
|
|
786
|
+
await runTestEffect(
|
|
787
|
+
PullRequestService.pipe(
|
|
788
|
+
Effect.flatMap((service) =>
|
|
789
|
+
service.setUrl(
|
|
790
|
+
"forked",
|
|
791
|
+
undefined,
|
|
792
|
+
"https://github.com/getpasted/pasted/pull/17",
|
|
793
|
+
root,
|
|
794
|
+
),
|
|
795
|
+
),
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
await Bun.write(
|
|
799
|
+
join(root, "bin", "gh"),
|
|
800
|
+
`#!/bin/sh
|
|
801
|
+
cat <<'JSON'
|
|
802
|
+
{"number":17,"state":"MERGED","title":"Polish README","isDraft":false,"headRefName":"task/polish-readme","baseRefName":"main","headRepository":{"nameWithOwner":"markjaquith/Pasted"},"baseRepository":{"nameWithOwner":"getpasted/pasted"},"url":"https://github.com/getpasted/pasted/pull/17","mergedAt":"2026-08-11T00:00:00Z","mergeCommit":{"oid":"241d45da1115ef685c91a5e6882d118c16801550"},"mergeable":"UNKNOWN"}
|
|
803
|
+
JSON
|
|
804
|
+
`,
|
|
805
|
+
)
|
|
806
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
807
|
+
|
|
808
|
+
const applied = await runTestEffect(
|
|
809
|
+
SyncService.pipe(
|
|
810
|
+
Effect.flatMap((service) =>
|
|
811
|
+
service.reconcile({ cwd: root, apply: true }),
|
|
812
|
+
),
|
|
813
|
+
),
|
|
814
|
+
)
|
|
815
|
+
expect(
|
|
816
|
+
applied.unresolved.filter((notice) => notice.target === "task:forked"),
|
|
817
|
+
).toEqual([])
|
|
818
|
+
expect(applied.changes.map((change) => change.kind)).toEqual([
|
|
819
|
+
"record-pr",
|
|
820
|
+
"mark-done",
|
|
821
|
+
])
|
|
822
|
+
const task = await runTestEffect(
|
|
823
|
+
TaskService.pipe(
|
|
824
|
+
Effect.flatMap((service) => service.show("forked", root)),
|
|
825
|
+
),
|
|
826
|
+
)
|
|
827
|
+
expect(task.data).toMatchObject({
|
|
828
|
+
status: "done",
|
|
829
|
+
pr: {
|
|
830
|
+
repository: "getpasted/pasted",
|
|
831
|
+
headRepository: "markjaquith/Pasted",
|
|
832
|
+
headBranch: "task/polish-readme",
|
|
833
|
+
baseRepository: "getpasted/pasted",
|
|
834
|
+
baseBranch: "main",
|
|
835
|
+
state: "merged",
|
|
836
|
+
merged: true,
|
|
837
|
+
},
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
|
|
750
841
|
test("leaves non-PR completion unchanged when a matching PR is discoverable", async () => {
|
|
751
842
|
await runTestEffect(
|
|
752
843
|
TaskService.pipe(
|
|
@@ -926,4 +1017,44 @@ exit 9
|
|
|
926
1017
|
),
|
|
927
1018
|
).toEqual([])
|
|
928
1019
|
})
|
|
1020
|
+
|
|
1021
|
+
test("keeps pull request query failures concise", async () => {
|
|
1022
|
+
await runTestEffect(
|
|
1023
|
+
TaskService.pipe(
|
|
1024
|
+
Effect.flatMap((service) =>
|
|
1025
|
+
service.create(
|
|
1026
|
+
{
|
|
1027
|
+
id: "unavailable",
|
|
1028
|
+
ticketUrl: null,
|
|
1029
|
+
repo: "agency",
|
|
1030
|
+
branch: "feat/unavailable",
|
|
1031
|
+
base: "main",
|
|
1032
|
+
},
|
|
1033
|
+
root,
|
|
1034
|
+
),
|
|
1035
|
+
),
|
|
1036
|
+
),
|
|
1037
|
+
)
|
|
1038
|
+
await Bun.write(
|
|
1039
|
+
join(root, "bin", "gh"),
|
|
1040
|
+
`#!/bin/sh
|
|
1041
|
+
echo "Unknown JSON field: unsupported" >&2
|
|
1042
|
+
echo "Available fields:" >&2
|
|
1043
|
+
echo " additions" >&2
|
|
1044
|
+
exit 1
|
|
1045
|
+
`,
|
|
1046
|
+
)
|
|
1047
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
1048
|
+
|
|
1049
|
+
const result = await runTestEffect(
|
|
1050
|
+
SyncService.pipe(
|
|
1051
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
1052
|
+
),
|
|
1053
|
+
)
|
|
1054
|
+
expect(result.warnings).toContainEqual({
|
|
1055
|
+
kind: "pr-discovery-unavailable",
|
|
1056
|
+
target: "task:unavailable",
|
|
1057
|
+
message: "Unknown JSON field: unsupported",
|
|
1058
|
+
})
|
|
1059
|
+
})
|
|
929
1060
|
})
|
|
@@ -153,6 +153,15 @@ const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
|
153
153
|
const originRef = (ref: string) =>
|
|
154
154
|
ref.replace(/^refs\/remotes\/origin\//, "").replace(/^origin\//, "")
|
|
155
155
|
|
|
156
|
+
const GITHUB_PR_FIELDS =
|
|
157
|
+
"number,state,title,isDraft,headRefName,baseRefName,headRepository,url,mergedAt,mergeCommit,mergeable"
|
|
158
|
+
|
|
159
|
+
const commandErrorSummary = (stderr: string, fallback: string) =>
|
|
160
|
+
stderr
|
|
161
|
+
.split("\n")
|
|
162
|
+
.map((line) => line.trim())
|
|
163
|
+
.find(Boolean) ?? fallback
|
|
164
|
+
|
|
156
165
|
export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
157
166
|
sync: () => ({
|
|
158
167
|
reconcile: (
|
|
@@ -349,7 +358,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
349
358
|
"view",
|
|
350
359
|
existing.url,
|
|
351
360
|
"--json",
|
|
352
|
-
|
|
361
|
+
GITHUB_PR_FIELDS,
|
|
353
362
|
],
|
|
354
363
|
{
|
|
355
364
|
cwd: repositoryPath,
|
|
@@ -372,7 +381,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
372
381
|
"--state",
|
|
373
382
|
"all",
|
|
374
383
|
"--json",
|
|
375
|
-
|
|
384
|
+
GITHUB_PR_FIELDS,
|
|
376
385
|
],
|
|
377
386
|
{
|
|
378
387
|
cwd: repositoryPath,
|
|
@@ -773,19 +782,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
773
782
|
const query = prQueries.get(record.key)!
|
|
774
783
|
const { remoteUrl, remoteRepository } = query
|
|
775
784
|
|
|
776
|
-
if (
|
|
777
|
-
existing &&
|
|
778
|
-
remoteRepository.toLowerCase() !== existing.repository.toLowerCase()
|
|
779
|
-
) {
|
|
780
|
-
prConflict = true
|
|
781
|
-
unresolved.push({
|
|
782
|
-
kind: "pr-repository-conflict",
|
|
783
|
-
target: record.key,
|
|
784
|
-
message: `Recorded PR repository does not match writable repository remote '${remoteName}'`,
|
|
785
|
-
action: "Correct the configured remote or recorded PR",
|
|
786
|
-
})
|
|
787
|
-
}
|
|
788
|
-
|
|
789
785
|
if (config.delivery && !remoteUrl) {
|
|
790
786
|
warnings.push({
|
|
791
787
|
kind: "delivery-remote-unavailable",
|
|
@@ -812,8 +808,9 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
812
808
|
} else if (
|
|
813
809
|
parsed.right &&
|
|
814
810
|
(parsed.right.provider !== config.delivery.provider ||
|
|
815
|
-
|
|
816
|
-
|
|
811
|
+
(
|
|
812
|
+
parsed.right.headRepository ?? parsed.right.repository
|
|
813
|
+
).toLowerCase() !== remoteRepository.toLowerCase())
|
|
817
814
|
) {
|
|
818
815
|
if (parsed.right) {
|
|
819
816
|
prConflict = true
|
|
@@ -836,8 +833,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
836
833
|
warnings.push({
|
|
837
834
|
kind: existing ? "pr-unavailable" : "pr-discovery-unavailable",
|
|
838
835
|
target: record.key,
|
|
839
|
-
message:
|
|
840
|
-
queried.stderr
|
|
836
|
+
message: commandErrorSummary(
|
|
837
|
+
queried.stderr,
|
|
838
|
+
"Could not query delivery provider",
|
|
839
|
+
),
|
|
841
840
|
})
|
|
842
841
|
}
|
|
843
842
|
} else if (existing) {
|
|
@@ -850,14 +849,18 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
850
849
|
current = recordFromGitHubJson(detail)
|
|
851
850
|
pr = { ...detail, ...current }
|
|
852
851
|
if (
|
|
853
|
-
|
|
854
|
-
|
|
852
|
+
current.headRepository?.toLowerCase() !==
|
|
853
|
+
remoteRepository.toLowerCase() ||
|
|
854
|
+
current.headBranch !== data.branch ||
|
|
855
|
+
current.baseRepository?.toLowerCase() !==
|
|
856
|
+
current.repository.toLowerCase() ||
|
|
857
|
+
current.baseBranch !== data.base
|
|
855
858
|
) {
|
|
856
859
|
prConflict = true
|
|
857
860
|
unresolved.push({
|
|
858
|
-
kind: "pr-
|
|
861
|
+
kind: "pr-repository-conflict",
|
|
859
862
|
target: record.key,
|
|
860
|
-
message: `Recorded PR
|
|
863
|
+
message: `Recorded PR head does not match '${remoteRepository}:${data.branch}' or base '${current.repository}:${data.base}'`,
|
|
861
864
|
action: "Correct the declaration or recorded PR URL",
|
|
862
865
|
})
|
|
863
866
|
}
|
|
@@ -866,7 +869,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
866
869
|
warnings.push({
|
|
867
870
|
kind: "pr-unavailable",
|
|
868
871
|
target: record.key,
|
|
869
|
-
message: `Could not inspect ${existing.url}: ${viewed.stderr
|
|
872
|
+
message: `Could not inspect ${existing.url}: ${commandErrorSummary(viewed.stderr, "GitHub query failed")}`,
|
|
870
873
|
})
|
|
871
874
|
}
|
|
872
875
|
} else {
|
|
@@ -883,6 +886,20 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
883
886
|
if (matches.length === 1) {
|
|
884
887
|
current = recordFromGitHubJson(matches[0]!)
|
|
885
888
|
pr = { ...matches[0], ...current }
|
|
889
|
+
if (
|
|
890
|
+
current.headRepository?.toLowerCase() !==
|
|
891
|
+
remoteRepository.toLowerCase() ||
|
|
892
|
+
current.baseRepository?.toLowerCase() !==
|
|
893
|
+
current.repository.toLowerCase()
|
|
894
|
+
) {
|
|
895
|
+
prConflict = true
|
|
896
|
+
unresolved.push({
|
|
897
|
+
kind: "pr-repository-conflict",
|
|
898
|
+
target: record.key,
|
|
899
|
+
message: `Discovered PR repositories do not match writable repository '${remoteRepository}' and base '${current.repository}'`,
|
|
900
|
+
action: "Record the authoritative PR URL manually",
|
|
901
|
+
})
|
|
902
|
+
}
|
|
886
903
|
} else if (matches.length > 1) {
|
|
887
904
|
unresolved.push({
|
|
888
905
|
kind: "multiple-prs",
|
|
@@ -895,8 +912,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
895
912
|
warnings.push({
|
|
896
913
|
kind: "pr-discovery-unavailable",
|
|
897
914
|
target: record.key,
|
|
898
|
-
message:
|
|
899
|
-
listed.stderr
|
|
915
|
+
message: commandErrorSummary(
|
|
916
|
+
listed.stderr,
|
|
917
|
+
"Could not discover pull requests",
|
|
918
|
+
),
|
|
900
919
|
})
|
|
901
920
|
}
|
|
902
921
|
}
|
|
@@ -104,8 +104,24 @@ describe("delivery commands", () => {
|
|
|
104
104
|
isDraft: false,
|
|
105
105
|
}
|
|
106
106
|
expect(
|
|
107
|
-
recordFromGitHubJson({
|
|
108
|
-
|
|
107
|
+
recordFromGitHubJson({
|
|
108
|
+
...base,
|
|
109
|
+
state: "OPEN",
|
|
110
|
+
headRefName: "feat/example",
|
|
111
|
+
baseRefName: "main",
|
|
112
|
+
headRepository: { nameWithOwner: "fork/agency" },
|
|
113
|
+
baseRepository: { nameWithOwner: "example/agency" },
|
|
114
|
+
mergeable: "MERGEABLE",
|
|
115
|
+
}),
|
|
116
|
+
).toMatchObject({
|
|
117
|
+
state: "open",
|
|
118
|
+
merged: false,
|
|
119
|
+
headRepository: "fork/agency",
|
|
120
|
+
headBranch: "feat/example",
|
|
121
|
+
baseRepository: "example/agency",
|
|
122
|
+
baseBranch: "main",
|
|
123
|
+
mergeable: true,
|
|
124
|
+
})
|
|
109
125
|
expect(
|
|
110
126
|
recordFromGitHubJson({
|
|
111
127
|
...base,
|
|
@@ -156,11 +156,24 @@ export const recordFromGitHubUrl = (url: string): PullRequestRecord => {
|
|
|
156
156
|
export const recordFromGitHubJson = (value: Record<string, unknown>) => {
|
|
157
157
|
const url = typeof value.url === "string" ? value.url : ""
|
|
158
158
|
const record = recordFromGitHubUrl(url)
|
|
159
|
+
const repositoryName = (repository: unknown) => {
|
|
160
|
+
if (!repository || typeof repository !== "object") return undefined
|
|
161
|
+
const nameWithOwner = (repository as Record<string, unknown>).nameWithOwner
|
|
162
|
+
return typeof nameWithOwner === "string" && nameWithOwner
|
|
163
|
+
? nameWithOwner
|
|
164
|
+
: undefined
|
|
165
|
+
}
|
|
159
166
|
const githubState = String(value.state ?? "OPEN").toLowerCase()
|
|
160
167
|
const merged = githubState === "merged" || value.mergedAt != null
|
|
161
168
|
const mergeable = String(value.mergeable ?? "UNKNOWN").toLowerCase()
|
|
162
169
|
return {
|
|
163
170
|
...record,
|
|
171
|
+
headRepository: repositoryName(value.headRepository),
|
|
172
|
+
headBranch:
|
|
173
|
+
typeof value.headRefName === "string" ? value.headRefName : undefined,
|
|
174
|
+
baseRepository: repositoryName(value.baseRepository) ?? record.repository,
|
|
175
|
+
baseBranch:
|
|
176
|
+
typeof value.baseRefName === "string" ? value.baseRefName : undefined,
|
|
164
177
|
state: merged ? "merged" : githubState === "closed" ? "closed" : "open",
|
|
165
178
|
draft: value.isDraft === true,
|
|
166
179
|
merged,
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -72,6 +72,10 @@ const GitHubPullRequestUrl = NonEmptyString.pipe(
|
|
|
72
72
|
export const PullRequestRecord = Schema.Struct({
|
|
73
73
|
provider: EntityId,
|
|
74
74
|
repository: NonEmptyString,
|
|
75
|
+
headRepository: Schema.optional(NonEmptyString),
|
|
76
|
+
headBranch: Schema.optional(NonEmptyString),
|
|
77
|
+
baseRepository: Schema.optional(NonEmptyString),
|
|
78
|
+
baseBranch: Schema.optional(NonEmptyString),
|
|
75
79
|
identifier: NonEmptyString,
|
|
76
80
|
url: Url,
|
|
77
81
|
state: Schema.Literal("open", "closed", "merged"),
|