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 | 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 | 4 | import { PUBLISH_LIMITS, enforcePublishLimits } from "../pipeline/publish"; |
| 5 | 5 | import type { AppSourceRef } from "../pipeline/descriptor"; |
| 6 | 6 | import type { AppsRuntime } from "../plugin/runtime"; |
| @@ -20,6 +20,7 @@ export interface GitHubSourceSnapshot { · 13 unmodified lines | ||
| 20 | 20 | readonly ref: string; |
| 21 | 21 | readonly upstreamSha: string; |
| 22 | 22 | readonly description?: string; |
| 23 | + readonly skipped: readonly GitHubSkippedArtifact[]; | |
| 23 | 24 | } |
| 24 | 25 | |
| 25 | 26 | export class GitHubSourceError extends Data.TaggedError("GitHubSourceError")<{ |
| @@ -35,27 +36,32 @@ export interface SyncErrorData { · 9 unmodified lines | ||
| 35 | 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 | 44 | export type GitHubSyncResult = |
| 39 | 45 | | { |
| 40 | 46 | readonly status: "published"; |
| 41 | 47 | readonly snapshotId: SnapshotId; |
| 42 | 48 | readonly upstreamSha: string; |
| 43 | 49 | readonly tools: readonly string[]; |
| 44 | − readonly skipped: readonly SkippedArtifact[]; | |
| 50 | + readonly skipped: readonly GitHubSkippedArtifact[]; | |
| 45 | 51 | readonly errors?: undefined; |
| 46 | 52 | } |
| 47 | 53 | | { |
| 48 | 54 | readonly status: "up-to-date"; |
| 49 | 55 | readonly upstreamSha: string; |
| 50 | 56 | readonly tools: readonly string[]; |
| 51 | − readonly skipped: readonly SkippedArtifact[]; | |
| 57 | + readonly skipped: readonly GitHubSkippedArtifact[]; | |
| 52 | 58 | readonly errors?: undefined; |
| 53 | 59 | } |
| 54 | 60 | | { |
| 55 | 61 | readonly status: "failed"; |
| 56 | 62 | readonly upstreamSha?: string; |
| 57 | 63 | readonly tools: readonly string[]; |
| 58 | − readonly skipped: readonly SkippedArtifact[]; | |
| 64 | + readonly skipped: readonly GitHubSkippedArtifact[]; | |
| 59 | 65 | readonly errors: readonly SyncErrorData[]; |
| 60 | 66 | }; |
| 61 | 67 | |
| @@ -65,7 +71,9 @@ export interface SyncGitHubSourceInput extends GitHubSourceInput { · 3 unmodified lines | ||
| 65 | 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 | 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 | 96 | return Effect.succeed(`${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.name)}`); |
| 89 | 97 | }; |
| 90 | 98 | |
| 91 | −const materializes = (path: string): boolean => | |
| 92 | − path === "executor.json" || MATERIALIZED_RE.test(path); | |
| 99 | +const acceptedPath = (path: string): boolean => path === "executor.json" || TOOL_RE.test(path); | |
| 93 | 100 | |
| 94 | 101 | const isRecord = (value: unknown): value is Record<string, unknown> => |
| 95 | 102 | value !== null && typeof value === "object" && !Array.isArray(value); |
| @@ -163,6 +170,7 @@ interface RefResponse { · 67 unmodified lines | ||
| 163 | 170 | interface TreeEntry { |
| 164 | 171 | readonly path?: unknown; |
| 165 | 172 | readonly type?: unknown; |
| 173 | + readonly mode?: unknown; | |
| 166 | 174 | readonly sha?: unknown; |
| 167 | 175 | readonly size?: unknown; |
| 168 | 176 | } |
| @@ -219,6 +227,34 @@ const checkTreeLimits = (entries: readonly TreeEntry[]): PublishError | null => · 50 unmodified lines | ||
| 219 | 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 | 258 | const decodeBlob = (path: string, blob: BlobResponse): Effect.Effect<string, GitHubSourceError> => |
| 223 | 259 | Effect.try({ |
| 224 | 260 | try: () => { |
| @@ -293,13 +329,14 @@ export const fetchGitHubSource = ( · 68 unmodified lines | ||
| 293 | 329 | path: `/repos/${repoPath}/git/trees/${treeSha}`, |
| 294 | 330 | }); |
| 295 | 331 | } |
| 296 | − const entries = (tree.tree ?? []).filter( | |
| 297 | − (entry) => | |
| 298 | − entry.type === "blob" && | |
| 299 | − typeof entry.path === "string" && | |
| 300 | − typeof entry.sha === "string" && | |
| 301 | − materializes(entry.path), | |
| 302 | − ); | |
| 332 | + const entries: (TreeEntry & { readonly path: string; readonly sha: string })[] = []; | |
| 333 | + const skipped: GitHubSkippedArtifact[] = []; | |
| 334 | + for (const entry of tree.tree ?? []) { | |
| 335 | + const classified = classifyTreeEntry(entry); | |
| 336 | + if (!classified) continue; | |
| 337 | + if (classified.kind === "fetch") entries.push(classified.entry); | |
| 338 | + else skipped.push(classified.skipped); | |
| 339 | + } | |
| 303 | 340 | const treeLimitError = checkTreeLimits(entries); |
| 304 | 341 | if (treeLimitError) return yield* Effect.fail(treeLimitError); |
| 305 | 342 | |
| @@ -321,6 +358,7 @@ export const fetchGitHubSource = ( · 15 unmodified lines | ||
| 321 | 358 | ref, |
| 322 | 359 | upstreamSha, |
| 323 | 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 | 410 | status: "up-to-date", |
| 373 | 411 | upstreamSha: snapshot.upstreamSha, |
| 374 | 412 | tools: current.tools.map((tool) => tool.name), |
| 375 | − skipped: current.skipped ?? [], | |
| 413 | + skipped: [...snapshot.skipped, ...(current.skipped ?? [])], | |
| 376 | 414 | } satisfies GitHubSyncResult; |
| 377 | 415 | } |
| 378 | 416 | |
| @@ -391,7 +429,7 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 12 unmodified lines | ||
| 391 | 429 | status: "failed", |
| 392 | 430 | upstreamSha: snapshot.upstreamSha, |
| 393 | 431 | tools: [], |
| 394 | − skipped: [], | |
| 432 | + skipped: snapshot.skipped, | |
| 395 | 433 | errors: [publishErrorToSyncError(published.failure)], |
| 396 | 434 | } satisfies GitHubSyncResult; |
| 397 | 435 | } |
| @@ -401,6 +439,6 @@ export const syncGitHubSource = (input: SyncGitHubSourceInput): Effect.Effect<Gi · 3 unmodified lines | ||
| 401 | 439 | snapshotId: published.success.snapshotId, |
| 402 | 440 | upstreamSha: snapshot.upstreamSha, |
| 403 | 441 | tools: published.success.descriptor.tools.map((tool) => tool.name), |
| 404 | − skipped: published.success.skipped, | |
| 442 | + skipped: [...snapshot.skipped, ...published.success.skipped], | |
| 405 | 443 | } satisfies GitHubSyncResult; |
| 406 | 444 | }); |