Showing the changes to packages/plugins/apps/src/source/github-source.ts (1 of 33 files). Show all
Unified · Split
| @@ -1,4 +1,4 @@ | |||
| 1 | import { Data, Effect, Result } from "effect"; | 1 | import { Data, Effect, Predicate, Result, Schema } from "effect"; |
| 2 | 2 | ||
| 3 | import { PublishError, type FileDiagnostic } from "../pipeline/discover"; | 3 | import { PublishError, type FileDiagnostic } from "../pipeline/discover"; |
| 4 | import { PUBLISH_LIMITS, enforcePublishLimits } from "../pipeline/publish"; | 4 | import { PUBLISH_LIMITS, enforcePublishLimits } from "../pipeline/publish"; |
| @@ -106,37 +106,42 @@ const requestJson = <A>( · 101 unmodified lines | |||
| 106 | input: GitHubSourceInput, | 106 | input: GitHubSourceInput, |
| 107 | path: string, | 107 | path: string, |
| 108 | ): Effect.Effect<A, GitHubSourceError> => | 108 | ): Effect.Effect<A, GitHubSourceError> => |
| 109 | Effect.tryPromise({ | 109 | Effect.gen(function* () { |
| 110 | try: async () => { | 110 | const response = yield* Effect.tryPromise({ |
| 111 | const fetchImpl = input.fetch ?? globalThis.fetch; | 111 | try: async () => { |
| 112 | const headers: Record<string, string> = { | 112 | const fetchImpl = input.fetch ?? globalThis.fetch; |
| 113 | accept: "application/vnd.github+json", | 113 | const headers: Record<string, string> = { |
| 114 | "user-agent": "executor-apps-github-source", | 114 | accept: "application/vnd.github+json", |
| 115 | }; | 115 | "user-agent": "executor-apps-github-source", |
| 116 | if (input.token) headers.authorization = `Bearer ${input.token}`; | 116 | }; |
| 117 | const response = await fetchImpl( | 117 | if (input.token) headers.authorization = `Bearer ${input.token}`; |
| 118 | `${trimBaseUrl(input.baseUrl ?? "https://api.github.com")}${path}`, | 118 | return fetchImpl(`${trimBaseUrl(input.baseUrl ?? "https://api.github.com")}${path}`, { |
| 119 | { | ||
| 120 | headers, | 119 | headers, |
| 121 | }, | ||
| 122 | ); | ||
| 123 | if (!response.ok) { | ||
| 124 | throw new GitHubSourceError({ | ||
| 125 | message: `GitHub request failed: GET ${path} -> ${response.status}`, | ||
| 126 | status: response.status, | ||
| 127 | path, | ||
| 128 | }); | 120 | }); |
| 129 | } | 121 | }, |
| 130 | return (await response.json()) as A; | 122 | catch: (cause) => |
| 131 | }, | 123 | new GitHubSourceError({ |
| 132 | catch: (cause) => | 124 | message: `GitHub request failed: GET ${path}`, |
| 133 | cause instanceof GitHubSourceError | 125 | path, |
| 134 | ? cause | 126 | cause, |
| 135 | : new GitHubSourceError({ | 127 | }), |
| 136 | message: `GitHub request failed: GET ${path}`, | 128 | }); |
| 137 | path, | 129 | if (!response.ok) { |
| 138 | cause, | 130 | return yield* new GitHubSourceError({ |
| 139 | }), | 131 | message: `GitHub request failed: GET ${path} -> ${response.status}`, |
| 132 | status: response.status, | ||
| 133 | path, | ||
| 134 | }); | ||
| 135 | } | ||
| 136 | return yield* Effect.tryPromise({ | ||
| 137 | try: () => response.json() as Promise<A>, | ||
| 138 | catch: (cause) => | ||
| 139 | new GitHubSourceError({ | ||
| 140 | message: `GitHub response was not valid JSON: GET ${path}`, | ||
| 141 | path, | ||
| 142 | cause, | ||
| 143 | }), | ||
| 144 | }); | ||
| 140 | }); | 145 | }); |
| 141 | 146 | ||
| 142 | interface RepoResponse { | 147 | interface RepoResponse { |
| @@ -254,42 +259,39 @@ const classifyTreeEntry = ( · 111 unmodified lines | |||
| 254 | }; | 259 | }; |
| 255 | 260 | ||
| 256 | const decodeBlob = (path: string, blob: BlobResponse): Effect.Effect<string, GitHubSourceError> => | 261 | const decodeBlob = (path: string, blob: BlobResponse): Effect.Effect<string, GitHubSourceError> => |
| 257 | Effect.try({ | 262 | Effect.gen(function* () { |
| 258 | try: () => { | 263 | const encoding = asString(blob.encoding); |
| 259 | const encoding = asString(blob.encoding); | 264 | const content = asString(blob.content); |
| 260 | const content = asString(blob.content); | 265 | if (!content || encoding !== "base64") { |
| 261 | if (!content || encoding !== "base64") { | 266 | return yield* new GitHubSourceError({ |
| 262 | throw new GitHubSourceError({ | 267 | message: `GitHub blob ${path} did not return base64 content`, |
| 263 | message: `GitHub blob ${path} did not return base64 content`, | 268 | path, |
| 264 | path, | 269 | }); |
| 265 | }); | 270 | } |
| 266 | } | 271 | return Buffer.from(content.replace(/\s/g, ""), "base64").toString("utf8"); |
| 267 | return Buffer.from(content.replace(/\s/g, ""), "base64").toString("utf8"); | ||
| 268 | }, | ||
| 269 | catch: (cause) => | ||
| 270 | cause instanceof GitHubSourceError | ||
| 271 | ? cause | ||
| 272 | : new GitHubSourceError({ message: `Failed to decode GitHub blob ${path}`, path, cause }), | ||
| 273 | }); | 272 | }); |
| 274 | 273 | ||
| 274 | const decodeExecutorJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); | ||
| 275 | |||
| 275 | const executorDescription = ( | 276 | const executorDescription = ( |
| 276 | files: FileSet, | 277 | files: FileSet, |
| 277 | ): Effect.Effect<string | undefined, GitHubSourceError> => | 278 | ): Effect.Effect<string | undefined, GitHubSourceError> => |
| 278 | Effect.try({ | 279 | Effect.gen(function* () { |
| 279 | try: () => { | 280 | const raw = files.get("executor.json"); |
| 280 | const raw = files.get("executor.json"); | 281 | if (!raw) return undefined; |
| 281 | if (!raw) return undefined; | 282 | const parsed = yield* decodeExecutorJson(raw).pipe( |
| 282 | const parsed = JSON.parse(raw) as unknown; | 283 | Effect.mapError( |
| 283 | if (!isRecord(parsed)) return undefined; | 284 | (cause) => |
| 284 | const description = parsed.description; | 285 | new GitHubSourceError({ |
| 285 | return typeof description === "string" ? description : undefined; | 286 | message: "executor.json is not valid JSON", |
| 286 | }, | 287 | path: "executor.json", |
| 287 | catch: (cause) => | 288 | cause, |
| 288 | new GitHubSourceError({ | 289 | }), |
| 289 | message: "executor.json is not valid JSON", | 290 | ), |
| 290 | path: "executor.json", | 291 | ); |
| 291 | cause, | 292 | if (!isRecord(parsed)) return undefined; |
| 292 | }), | 293 | const description = parsed.description; |
| 294 | return typeof description === "string" ? description : undefined; | ||
| 293 | }); | 295 | }); |
| 294 | 296 | ||
| 295 | export const fetchGitHubSource = ( | 297 | export const fetchGitHubSource = ( |
| @@ -336,7 +338,7 @@ export const fetchGitHubSource = ( · 40 unmodified lines | |||
| 336 | else skipped.push(classified.skipped); | 338 | else skipped.push(classified.skipped); |
| 337 | } | 339 | } |
| 338 | const treeLimitError = checkTreeLimits(entries); | 340 | const treeLimitError = checkTreeLimits(entries); |
| 339 | if (treeLimitError) return yield* Effect.fail(treeLimitError); | 341 | if (treeLimitError) return yield* treeLimitError; |
| 340 | 342 | ||
| 341 | const files = new Map<string, string>(); | 343 | const files = new Map<string, string>(); |
| 342 | for (const entry of entries) { | 344 | for (const entry of entries) { |
| @@ -348,7 +350,7 @@ export const fetchGitHubSource = ( · 5 unmodified lines | |||
| 348 | files.set(path, yield* decodeBlob(path, blob)); | 350 | files.set(path, yield* decodeBlob(path, blob)); |
| 349 | } | 351 | } |
| 350 | const payloadLimitError = enforcePublishLimits(files); | 352 | const payloadLimitError = enforcePublishLimits(files); |
| 351 | if (payloadLimitError) return yield* Effect.fail(payloadLimitError); | 353 | if (payloadLimitError) return yield* payloadLimitError; |
| 352 | 354 | ||
| 353 | return { | 355 | return { |
| 354 | files, | 356 | files, |
| @@ -360,17 +362,22 @@ export const fetchGitHubSource = ( · 5 unmodified lines | |||
| 360 | }; | 362 | }; |
| 361 | }); | 363 | }); |
| 362 | 364 | ||
| 363 | const publishErrorToSyncError = (error: PublishError): SyncErrorData => ({ | 365 | const publishErrorToSyncError = (publishFailure: PublishError): SyncErrorData => { |
| 364 | stage: error.stage, | 366 | const stage = publishFailure.stage; |
| 365 | message: error.message, | 367 | const message = publishFailure.message; |
| 366 | diagnostics: error.diagnostics, | 368 | const diagnostics = publishFailure.diagnostics; |
| 367 | }); | 369 | return { stage, message, diagnostics }; |
| 370 | }; | ||
| 368 | 371 | ||
| 369 | const sourceErrorToSyncError = (error: GitHubSourceError): SyncErrorData => ({ | 372 | const sourceErrorToSyncError = (sourceFailure: GitHubSourceError): SyncErrorData => { |
| 370 | stage: "source", | 373 | const message = sourceFailure.message; |
| 371 | message: error.message, | 374 | const path = sourceFailure.path; |
| 372 | diagnostics: error.path ? [{ path: error.path, message: error.message }] : [], | 375 | return { |
| 373 | }); | 376 | stage: "source", |
| 377 | message, | ||
| 378 | diagnostics: path ? [{ path, message }] : [], | ||
| 379 | }; | ||
| 380 | }; | ||
| 374 | 381 | ||
| 375 | const sourceRef = (snapshot: GitHubSourceSnapshot, connection?: string): AppSourceRef => ({ | 382 | const sourceRef = (snapshot: GitHubSourceSnapshot, connection?: string): AppSourceRef => ({ |
| 376 | kind: "github", | 383 | kind: "github", |
| @@ -391,7 +398,7 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 14 unmodified lines | |||
| 391 | tools: [], | 398 | tools: [], |
| 392 | skipped: [], | 399 | skipped: [], |
| 393 | errors: [ | 400 | errors: [ |
| 394 | error instanceof PublishError | 401 | Predicate.isTagged("PublishError")(error) |
| 395 | ? publishErrorToSyncError(error) | 402 | ? publishErrorToSyncError(error) |
| 396 | : sourceErrorToSyncError(error), | 403 | : sourceErrorToSyncError(error), |
| 397 | ], | 404 | ], |