Showing the changes to apps/server/src/sourceControl/GitHubSourceControlProvider.ts (1 of 59 files). Show all
Unified · Split
| @@ -5,11 +5,15 @@ import { · 4 unmodified lines | |||
| 5 | type ChangeRequestState, | 5 | type ChangeRequestState, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | ||
| 8 | import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts"; | 8 | import * as GitHubCli from "./GitHubCli.ts"; |
| 9 | import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts"; | 9 | import * as GitHubPullRequests from "./gitHubPullRequests.ts"; |
| 10 | import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts"; | 10 | import * as SourceControlProvider from "./SourceControlProvider.ts"; |
| 11 | import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | ||
| 11 | 12 | ||
| 12 | function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError { | 13 | function providerError( |
| 14 | operation: string, | ||
| 15 | cause: GitHubCli.GitHubCliError, | ||
| 16 | ): SourceControlProviderError { | ||
| 13 | return new SourceControlProviderError({ | 17 | return new SourceControlProviderError({ |
| 14 | provider: "github", | 18 | provider: "github", |
| 15 | operation, | 19 | operation, |
| @@ -18,7 +22,7 @@ function providerError(operation: string, cause: GitHubCliError): SourceControlP · 2 unmodified lines | |||
| 18 | }); | 22 | }); |
| 19 | } | 23 | } |
| 20 | 24 | ||
| 21 | function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { | 25 | function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeRequest { |
| 22 | return { | 26 | return { |
| 23 | provider: "github", | 27 | provider: "github", |
| 24 | number: summary.number, | 28 | number: summary.number, |
| @@ -40,75 +44,119 @@ function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { · 15 unmodified lines | |||
| 40 | }; | 44 | }; |
| 41 | } | 45 | } |
| 42 | 46 | ||
| 47 | function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { | ||
| 48 | const output = SourceControlProviderDiscovery.combinedAuthOutput(input); | ||
| 49 | const account = SourceControlProviderDiscovery.matchFirst(output, [ | ||
| 50 | /Logged in to .* account\s+([^\s(]+)/iu, | ||
| 51 | /Logged in to .* as\s+([^\s(]+)/iu, | ||
| 52 | ]); | ||
| 53 | const host = SourceControlProviderDiscovery.parseCliHost(output); | ||
| 54 | |||
| 55 | if (input.exitCode !== 0) { | ||
| 56 | return SourceControlProviderDiscovery.providerAuth({ | ||
| 57 | status: "unauthenticated", | ||
| 58 | host, | ||
| 59 | detail: | ||
| 60 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 61 | "Run `gh auth login` to authenticate GitHub CLI.", | ||
| 62 | }); | ||
| 63 | } | ||
| 64 | |||
| 65 | if (account) { | ||
| 66 | return SourceControlProviderDiscovery.providerAuth({ status: "authenticated", account, host }); | ||
| 67 | } | ||
| 68 | |||
| 69 | return SourceControlProviderDiscovery.providerAuth({ | ||
| 70 | status: "unknown", | ||
| 71 | host, | ||
| 72 | detail: | ||
| 73 | SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | ||
| 74 | "GitHub CLI auth status could not be parsed.", | ||
| 75 | }); | ||
| 76 | } | ||
| 77 | |||
| 78 | export const discovery = { | ||
| 79 | type: "cli", | ||
| 80 | kind: "github", | ||
| 81 | label: "GitHub", | ||
| 82 | executable: "gh", | ||
| 83 | versionArgs: ["--version"], | ||
| 84 | authArgs: ["auth", "status"], | ||
| 85 | parseAuth: parseGitHubAuth, | ||
| 86 | installHint: | ||
| 87 | "Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).", | ||
| 88 | } satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; | ||
| 89 | |||
| 43 | export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { | 90 | export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { |
| 44 | const github = yield* GitHubCli; | 91 | const github = yield* GitHubCli.GitHubCli; |
| 45 | 92 | ||
| 46 | const listChangeRequests: SourceControlProviderShape["listChangeRequests"] = (input) => { | 93 | const listChangeRequests: SourceControlProvider.SourceControlProviderShape["listChangeRequests"] = |
| 47 | if (input.state === "open") { | 94 | (input) => { |
| 95 | if (input.state === "open") { | ||
| 96 | return github | ||
| 97 | .listOpenPullRequests({ | ||
| 98 | cwd: input.cwd, | ||
| 99 | headSelector: input.headSelector, | ||
| 100 | ...(input.limit !== undefined ? { limit: input.limit } : {}), | ||
| 101 | }) | ||
| 102 | .pipe( | ||
| 103 | Effect.map((items) => items.map(toChangeRequest)), | ||
| 104 | Effect.mapError((error) => providerError("listChangeRequests", error)), | ||
| 105 | ); | ||
| 106 | } | ||
| 107 | |||
| 108 | const stateArg: ChangeRequestState | "all" = input.state; | ||
| 48 | return github | 109 | return github |
| 49 | .listOpenPullRequests({ | 110 | .execute({ |
| 50 | cwd: input.cwd, | 111 | cwd: input.cwd, |
| 51 | headSelector: input.headSelector, | 112 | args: [ |
| 52 | ...(input.limit !== undefined ? { limit: input.limit } : {}), | 113 | "pr", |
| 114 | "list", | ||
| 115 | "--head", | ||
| 116 | input.headSelector, | ||
| 117 | "--state", | ||
| 118 | stateArg, | ||
| 119 | "--limit", | ||
| 120 | String(input.limit ?? 20), | ||
| 121 | "--json", | ||
| 122 | "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner", | ||
| 123 | ], | ||
| 53 | }) | 124 | }) |
| 54 | .pipe( | 125 | .pipe( |
| 55 | Effect.map((items) => items.map(toChangeRequest)), | 126 | Effect.flatMap((result) => { |
| 56 | Effect.mapError((error) => providerError("listChangeRequests", error)), | 127 | const raw = result.stdout.trim(); |
| 128 | if (raw.length === 0) { | ||
| 129 | return Effect.succeed([]); | ||
| 130 | } | ||
| 131 | return Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestListJson(raw)).pipe( | ||
| 132 | Effect.flatMap((decoded) => | ||
| 133 | Result.isSuccess(decoded) | ||
| 134 | ? Effect.succeed( | ||
| 135 | decoded.success.map((item) => ({ | ||
| 136 | ...toChangeRequest(item), | ||
| 137 | updatedAt: item.updatedAt, | ||
| 138 | })), | ||
| 139 | ) | ||
| 140 | : Effect.fail( | ||
| 141 | new SourceControlProviderError({ | ||
| 142 | provider: "github", | ||
| 143 | operation: "listChangeRequests", | ||
| 144 | detail: "GitHub CLI returned invalid change request JSON.", | ||
| 145 | cause: decoded.failure, | ||
| 146 | }), | ||
| 147 | ), | ||
| 148 | ), | ||
| 149 | ); | ||
| 150 | }), | ||
| 151 | Effect.mapError((error) => | ||
| 152 | Schema.is(SourceControlProviderError)(error) | ||
| 153 | ? error | ||
| 154 | : providerError("listChangeRequests", error), | ||
| 155 | ), | ||
| 57 | ); | 156 | ); |
| 58 | } | 157 | }; |
| 59 | |||
| 60 | const stateArg: ChangeRequestState | "all" = input.state; | ||
| 61 | return github | ||
| 62 | .execute({ | ||
| 63 | cwd: input.cwd, | ||
| 64 | args: [ | ||
| 65 | "pr", | ||
| 66 | "list", | ||
| 67 | "--head", | ||
| 68 | input.headSelector, | ||
| 69 | "--state", | ||
| 70 | stateArg, | ||
| 71 | "--limit", | ||
| 72 | String(input.limit ?? 20), | ||
| 73 | "--json", | ||
| 74 | "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner", | ||
| 75 | ], | ||
| 76 | }) | ||
| 77 | .pipe( | ||
| 78 | Effect.flatMap((result) => { | ||
| 79 | const raw = result.stdout.trim(); | ||
| 80 | if (raw.length === 0) { | ||
| 81 | return Effect.succeed([]); | ||
| 82 | } | ||
| 83 | return Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe( | ||
| 84 | Effect.flatMap((decoded) => | ||
| 85 | Result.isSuccess(decoded) | ||
| 86 | ? Effect.succeed( | ||
| 87 | decoded.success.map((item) => ({ | ||
| 88 | ...toChangeRequest(item), | ||
| 89 | updatedAt: item.updatedAt, | ||
| 90 | })), | ||
| 91 | ) | ||
| 92 | : Effect.fail( | ||
| 93 | new SourceControlProviderError({ | ||
| 94 | provider: "github", | ||
| 95 | operation: "listChangeRequests", | ||
| 96 | detail: "GitHub CLI returned invalid change request JSON.", | ||
| 97 | cause: decoded.failure, | ||
| 98 | }), | ||
| 99 | ), | ||
| 100 | ), | ||
| 101 | ); | ||
| 102 | }), | ||
| 103 | Effect.mapError((error) => | ||
| 104 | Schema.is(SourceControlProviderError)(error) | ||
| 105 | ? error | ||
| 106 | : providerError("listChangeRequests", error), | ||
| 107 | ), | ||
| 108 | ); | ||
| 109 | }; | ||
| 110 | 158 | ||
| 111 | return SourceControlProvider.of({ | 159 | return SourceControlProvider.SourceControlProvider.of({ |
| 112 | kind: "github", | 160 | kind: "github", |
| 113 | listChangeRequests, | 161 | listChangeRequests, |
| 114 | getChangeRequest: (input) => | 162 | getChangeRequest: (input) => |
| @@ -145,4 +193,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { · 30 unmodified lines | |||
| 145 | }); | 193 | }); |
| 146 | }); | 194 | }); |
| 147 | 195 | ||
| 148 | export const layer = Layer.effect(SourceControlProvider, make()); | 196 | export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); |