From de23a6479786139a9b54fa4d7b269383391bd06b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 30 Jun 2026 14:06:59 -0400 Subject: [PATCH] fix(gh): tolerate non-JSON warnings before gh pr view output gh can emit warning lines (e.g. auth expiry notices) to stdout before the JSON payload, causing JSON.parse to fail with 'returned invalid JSON' even though the JSON itself is valid. The decoder now extracts the JSON payload by finding the first { or [ and matching closer, so leading or trailing non-JSON output is stripped before parsing. Fixes #12. --- .changeset/tolerate-gh-warnings.md | 5 +++++ src/services/code-host/GitHub.ts | 16 +++++++++++--- tests/stack.test.ts | 34 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 .changeset/tolerate-gh-warnings.md diff --git a/.changeset/tolerate-gh-warnings.md b/.changeset/tolerate-gh-warnings.md new file mode 100644 index 0000000..e7a2d5e --- /dev/null +++ b/.changeset/tolerate-gh-warnings.md @@ -0,0 +1,5 @@ +--- +"@kitlangton/stack": patch +--- + +Tolerate non-JSON warnings (e.g. auth expiry notices) emitted before `gh pr view` JSON output. The decoder now extracts the JSON payload from stdout instead of requiring the entire output to be valid JSON. diff --git a/src/services/code-host/GitHub.ts b/src/services/code-host/GitHub.ts index 2d18f60..3209bc0 100644 --- a/src/services/code-host/GitHub.ts +++ b/src/services/code-host/GitHub.ts @@ -51,21 +51,31 @@ class PullListData extends Schema.Class("PullListData")({ const PullListJson = Schema.Array(Schema.Array(PullListData)); +const extractJson = (out: string): string => { + const start = out.search(/[[{]/); + if (start === -1) return out; + const opener = out[start]; + const closer = opener === "{" ? "}" : "]"; + const end = out.lastIndexOf(closer); + if (end === -1 || end < start) return out; + return out.slice(start, end + 1); +}; + const decodePullList = (args: ReadonlyArray, out: string) => Effect.try({ - try: () => Schema.decodeUnknownSync(PullListJson)(JSON.parse(out)), + try: () => Schema.decodeUnknownSync(PullListJson)(JSON.parse(extractJson(out))), catch: (err) => new CodeHostDecodeError("gh", args, out, String(err)), }); const decodePullView = (args: ReadonlyArray, out: string) => Effect.try({ - try: () => Schema.decodeUnknownSync(PullView)(JSON.parse(out)), + try: () => Schema.decodeUnknownSync(PullView)(JSON.parse(extractJson(out))), catch: (err) => new CodeHostDecodeError("gh", args, out, String(err)), }); const decodePullWatch = (args: ReadonlyArray, out: string) => Effect.try({ - try: () => Schema.decodeUnknownSync(PullWatch)(JSON.parse(out)), + try: () => Schema.decodeUnknownSync(PullWatch)(JSON.parse(extractJson(out))), catch: (err) => new CodeHostDecodeError("gh", args, out, String(err)), }); diff --git a/tests/stack.test.ts b/tests/stack.test.ts index 3285e2d..93fd795 100644 --- a/tests/stack.test.ts +++ b/tests/stack.test.ts @@ -1611,6 +1611,40 @@ describe("GitHub", () => { ); }); + it.effect("tolerates non-JSON warnings emitted before gh pr view output", () => { + const proc = Layer.succeed( + Proc.Service, + Proc.Service.of({ + exec: () => + Effect.sync(() => + [ + "warning: authentication token expires soon, run gh auth refresh", + JSON.stringify({ + number: 1, + title: "one", + body: "body", + headRefName: "one", + headRepository: { nameWithOwner: "owner/project" }, + baseRefName: "main", + url: "u1", + isDraft: false, + labels: [], + }), + ].join("\n"), + ), + }), + ); + + return Effect.gen(function* () { + const github = yield* CodeHost.Service; + const meta = yield* github.change(1); + expect(Number(meta.number)).toBe(1); + expect(meta.title).toBe("one"); + }).pipe( + Effect.provide(CodeHostGitHub.layer.pipe(Layer.provideMerge(cfg), Layer.provideMerge(proc))), + ); + }); + it.effect("resolves a created GitHub fork PR by its returned URL", () => { const calls: Array> = []; const proc = Layer.succeed(