Showing the changes to packages/plugins/apps/src/source/github-source.ts (1 of 4 files). Show all
Unified · Split
| @@ -1,6 +1,6 @@ | |||
| 1 | import { Data, Effect, Result } from "effect"; | 1 | import { Data, Effect, Result } from "effect"; |
| 2 | 2 | ||
| 3 | import { PublishError, type FileDiagnostic, type SkippedArtifact } 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"; |
| 5 | import type { AppSourceRef } from "../pipeline/descriptor"; | 5 | import type { AppSourceRef } from "../pipeline/descriptor"; |
| 6 | import type { AppsRuntime } from "../plugin/runtime"; | 6 | import type { AppsRuntime } from "../plugin/runtime"; |
| @@ -20,6 +20,7 @@ export interface GitHubSourceSnapshot { · 13 unmodified lines | |||
| 20 | readonly ref: string; | 20 | readonly ref: string; |
| 21 | readonly upstreamSha: string; | 21 | readonly upstreamSha: string; |
| 22 | readonly description?: string; | 22 | readonly description?: string; |
| 23 | readonly skipped: readonly GitHubSkippedArtifact[]; | ||
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | export class GitHubSourceError extends Data.TaggedError("GitHubSourceError")<{ | 26 | export class GitHubSourceError extends Data.TaggedError("GitHubSourceError")<{ |
| @@ -35,27 +36,32 @@ export interface SyncErrorData { · 9 unmodified lines | |||
| 35 | readonly diagnostics?: readonly FileDiagnostic[]; | 36 | readonly diagnostics?: readonly FileDiagnostic[]; |
| 36 | } | 37 | } |
| 37 | 38 | ||
| 39 | export interface GitHubSkippedArtifact { | ||
| 40 | readonly path: string; | ||
| 41 | readonly reason: "not supported yet" | "unsupported file type" | "ignored"; | ||
| 42 | } | ||
| 43 | |||
| 38 | export type GitHubSyncResult = | 44 | export type GitHubSyncResult = |
| 39 | | { | 45 | | { |
| 40 | readonly status: "published"; | 46 | readonly status: "published"; |
| 41 | readonly snapshotId: SnapshotId; | 47 | readonly snapshotId: SnapshotId; |
| 42 | readonly upstreamSha: string; | 48 | readonly upstreamSha: string; |
| 43 | readonly tools: readonly string[]; | 49 | readonly tools: readonly string[]; |
| 44 | readonly skipped: readonly SkippedArtifact[]; | 50 | readonly skipped: readonly GitHubSkippedArtifact[]; |
| 45 | readonly errors?: undefined; | 51 | readonly errors?: undefined; |
| 46 | } | 52 | } |
| 47 | | { | 53 | | { |
| 48 | readonly status: "up-to-date"; | 54 | readonly status: "up-to-date"; |
| 49 | readonly upstreamSha: string; | 55 | readonly upstreamSha: string; |
| 50 | readonly tools: readonly string[]; | 56 | readonly tools: readonly string[]; |
| 51 | readonly skipped: readonly SkippedArtifact[]; | 57 | readonly skipped: readonly GitHubSkippedArtifact[]; |
| 52 | readonly errors?: undefined; | 58 | readonly errors?: undefined; |
| 53 | } | 59 | } |
| 54 | | { | 60 | | { |
| 55 | readonly status: "failed"; | 61 | readonly status: "failed"; |
| 56 | readonly upstreamSha?: string; | 62 | readonly upstreamSha?: string; |
| 57 | readonly tools: readonly string[]; | 63 | readonly tools: readonly string[]; |
| 58 | readonly skipped: readonly SkippedArtifact[]; | 64 | readonly skipped: readonly GitHubSkippedArtifact[]; |
| 59 | readonly errors: readonly SyncErrorData[]; | 65 | readonly errors: readonly SyncErrorData[]; |
| 60 | }; | 66 | }; |
| 61 | 67 | ||
| @@ -65,7 +71,9 @@ export interface SyncGitHubSourceInput extends GitHubSourceInput { · 3 unmodified lines | |||
| 65 | readonly scope: string; | 71 | readonly scope: string; |
| 66 | } | 72 | } |
| 67 | 73 | ||
| 68 | const MATERIALIZED_RE = /^(tools|workflows|ui|skills)\//; | 74 | const TOOL_RE = /^tools\/([a-z0-9][a-z0-9-]*)\.(ts|tsx|js|jsx)$/; |
| 75 | const DEFERRED_RE = /^(workflows|ui|skills)\//; | ||
| 76 | const REGULAR_FILE_MODES = new Set(["100644", "100755"]); | ||
| 69 | 77 | ||
| 70 | const trimBaseUrl = (baseUrl: string): string => baseUrl.replace(/\/+$/, ""); | 78 | const trimBaseUrl = (baseUrl: string): string => baseUrl.replace(/\/+$/, ""); |
| 71 | 79 | ||
| @@ -88,8 +96,7 @@ const encodedRepoPath = (repo: string): Effect.Effect<string, GitHubSourceError> · 16 unmodified lines | |||
| 88 | return Effect.succeed(`${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}`); | 96 | return Effect.succeed(`${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}`); |
| 89 | }; | 97 | }; |
| 90 | 98 | ||
| 91 | const materializes = (path: string): boolean => | 99 | const acceptedPath = (path: string): boolean => path === "executor.json" || TOOL_RE.test(path); |
| 92 | path === "executor.json" || MATERIALIZED_RE.test(path); | ||
| 93 | 100 | ||
| 94 | const isRecord = (value: unknown): value is Record<string, unknown> => | 101 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 95 | value !== null && typeof value === "object" && !Array.isArray(value); | 102 | value !== null && typeof value === "object" && !Array.isArray(value); |
| @@ -163,6 +170,7 @@ interface RefResponse { · 67 unmodified lines | |||
| 163 | interface TreeEntry { | 170 | interface TreeEntry { |
| 164 | readonly path?: unknown; | 171 | readonly path?: unknown; |
| 165 | readonly type?: unknown; | 172 | readonly type?: unknown; |
| 173 | readonly mode?: unknown; | ||
| 166 | readonly sha?: unknown; | 174 | readonly sha?: unknown; |
| 167 | readonly size?: unknown; | 175 | readonly size?: unknown; |
| 168 | } | 176 | } |
| @@ -219,6 +227,34 @@ const checkTreeLimits = (entries: readonly TreeEntry[]): PublishError | null => · 50 unmodified lines | |||
| 219 | return diagnostics.length === 0 ? null : limitError(diagnostics); | 227 | return diagnostics.length === 0 ? null : limitError(diagnostics); |
| 220 | }; | 228 | }; |
| 221 | 229 | ||
| 230 | const classifyTreeEntry = ( | ||
| 231 | entry: TreeEntry, | ||
| 232 | ): | ||
| 233 | | { | ||
| 234 | readonly kind: "fetch"; | ||
| 235 | readonly entry: TreeEntry & { readonly path: string; readonly sha: string }; | ||
| 236 | } | ||
| 237 | | { readonly kind: "skip"; readonly skipped: GitHubSkippedArtifact } | ||
| 238 | | null => { | ||
| 239 | const path = asString(entry.path); | ||
| 240 | if (!path) return null; | ||
| 241 | |||
| 242 | const type = asString(entry.type); | ||
| 243 | const mode = asString(entry.mode); | ||
| 244 | if (type === "tree") return null; | ||
| 245 | if (type !== "blob" || !mode || !REGULAR_FILE_MODES.has(mode)) { | ||
| 246 | return { kind: "skip", skipped: { path, reason: "unsupported file type" } }; | ||
| 247 | } | ||
| 248 | |||
| 249 | const sha = asString(entry.sha); | ||
| 250 | if (!sha) return { kind: "skip", skipped: { path, reason: "unsupported file type" } }; | ||
| 251 | if (acceptedPath(path)) return { kind: "fetch", entry: { ...entry, path, sha } }; | ||
| 252 | if (DEFERRED_RE.test(path)) { | ||
| 253 | return { kind: "skip", skipped: { path, reason: "not supported yet" } }; | ||
| 254 | } | ||
| 255 | return { kind: "skip", skipped: { path, reason: "ignored" } }; | ||
| 256 | }; | ||
| 257 | |||
| 222 | const decodeBlob = (path: string, blob: BlobResponse): Effect.Effect<string, GitHubSourceError> => | 258 | const decodeBlob = (path: string, blob: BlobResponse): Effect.Effect<string, GitHubSourceError> => |
| 223 | Effect.try({ | 259 | Effect.try({ |
| 224 | try: () => { | 260 | try: () => { |
| @@ -293,13 +329,14 @@ export const fetchGitHubSource = ( · 68 unmodified lines | |||
| 293 | path: `/repos/${repoPath}/git/trees/${treeSha}`, | 329 | path: `/repos/${repoPath}/git/trees/${treeSha}`, |
| 294 | }); | 330 | }); |
| 295 | } | 331 | } |
| 296 | const entries = (tree.tree ?? []).filter( | 332 | const entries: (TreeEntry & { readonly path: string; readonly sha: string })[] = []; |
| 297 | (entry) => | 333 | const skipped: GitHubSkippedArtifact[] = []; |
| 298 | entry.type === "blob" && | 334 | for (const entry of tree.tree ?? []) { |
| 299 | typeof entry.path === "string" && | 335 | const classified = classifyTreeEntry(entry); |
| 300 | typeof entry.sha === "string" && | 336 | if (!classified) continue; |
| 301 | materializes(entry.path), | 337 | if (classified.kind === "fetch") entries.push(classified.entry); |
| 302 | ); | 338 | else skipped.push(classified.skipped); |
| 339 | } | ||
| 303 | const treeLimitError = checkTreeLimits(entries); | 340 | const treeLimitError = checkTreeLimits(entries); |
| 304 | if (treeLimitError) return yield* Effect.fail(treeLimitError); | 341 | if (treeLimitError) return yield* Effect.fail(treeLimitError); |
| 305 | 342 | ||
| @@ -321,6 +358,7 @@ export const fetchGitHubSource = ( · 15 unmodified lines | |||
| 321 | ref, | 358 | ref, |
| 322 | upstreamSha, | 359 | upstreamSha, |
| 323 | description: yield* executorDescription(files), | 360 | description: yield* executorDescription(files), |
| 361 | skipped, | ||
| 324 | }; | 362 | }; |
| 325 | }); | 363 | }); |
| 326 | 364 | ||
| @@ -372,7 +410,7 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 45 unmodified lines | |||
| 372 | status: "up-to-date", | 410 | status: "up-to-date", |
| 373 | upstreamSha: snapshot.upstreamSha, | 411 | upstreamSha: snapshot.upstreamSha, |
| 374 | tools: current.tools.map((tool) => tool.name), | 412 | tools: current.tools.map((tool) => tool.name), |
| 375 | skipped: current.skipped ?? [], | 413 | skipped: [...snapshot.skipped, ...(current.skipped ?? [])], |
| 376 | } satisfies GitHubSyncResult; | 414 | } satisfies GitHubSyncResult; |
| 377 | } | 415 | } |
| 378 | 416 | ||
| @@ -391,7 +429,7 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 12 unmodified lines | |||
| 391 | status: "failed", | 429 | status: "failed", |
| 392 | upstreamSha: snapshot.upstreamSha, | 430 | upstreamSha: snapshot.upstreamSha, |
| 393 | tools: [], | 431 | tools: [], |
| 394 | skipped: [], | 432 | skipped: snapshot.skipped, |
| 395 | errors: [publishErrorToSyncError(published.failure)], | 433 | errors: [publishErrorToSyncError(published.failure)], |
| 396 | } satisfies GitHubSyncResult; | 434 | } satisfies GitHubSyncResult; |
| 397 | } | 435 | } |
| @@ -401,6 +439,6 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 3 unmodified lines | |||
| 401 | snapshotId: published.success.snapshotId, | 439 | snapshotId: published.success.snapshotId, |
| 402 | upstreamSha: snapshot.upstreamSha, | 440 | upstreamSha: snapshot.upstreamSha, |
| 403 | tools: published.success.descriptor.tools.map((tool) => tool.name), | 441 | tools: published.success.descriptor.tools.map((tool) => tool.name), |
| 404 | skipped: published.success.skipped, | 442 | skipped: [...snapshot.skipped, ...published.success.skipped], |
| 405 | } satisfies GitHubSyncResult; | 443 | } satisfies GitHubSyncResult; |
| 406 | }); | 444 | }); |