@shivaedev/github 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +115 -7
  3. package/dist/GitHub.d.ts +279 -19
  4. package/dist/GitHub.d.ts.map +1 -1
  5. package/dist/GitHub.js +22 -10
  6. package/dist/GitHub.js.map +1 -1
  7. package/dist/adapters/manualRedirects.d.ts +4 -0
  8. package/dist/adapters/manualRedirects.d.ts.map +1 -0
  9. package/dist/adapters/manualRedirects.js +6 -0
  10. package/dist/adapters/manualRedirects.js.map +1 -0
  11. package/dist/adapters/mergeResponseHeaders.d.ts +3 -0
  12. package/dist/adapters/mergeResponseHeaders.d.ts.map +1 -0
  13. package/dist/adapters/mergeResponseHeaders.js +5 -0
  14. package/dist/adapters/mergeResponseHeaders.js.map +1 -0
  15. package/dist/adapters/resolveApiUrl.d.ts +2 -0
  16. package/dist/adapters/resolveApiUrl.d.ts.map +1 -0
  17. package/dist/adapters/resolveApiUrl.js +6 -0
  18. package/dist/adapters/resolveApiUrl.js.map +1 -0
  19. package/dist/adapters/signAppJwt.d.ts +5 -0
  20. package/dist/adapters/signAppJwt.d.ts.map +1 -0
  21. package/dist/adapters/signAppJwt.js +15 -0
  22. package/dist/adapters/signAppJwt.js.map +1 -0
  23. package/dist/adapters/verifySignature.d.ts +4 -0
  24. package/dist/adapters/verifySignature.d.ts.map +1 -0
  25. package/dist/adapters/verifySignature.js +13 -0
  26. package/dist/adapters/verifySignature.js.map +1 -0
  27. package/dist/adapters/zipEntries.d.ts +17 -0
  28. package/dist/adapters/zipEntries.d.ts.map +1 -0
  29. package/dist/adapters/zipEntries.js +57 -0
  30. package/dist/adapters/zipEntries.js.map +1 -0
  31. package/dist/appJwt.d.ts +2 -3
  32. package/dist/appJwt.d.ts.map +1 -1
  33. package/dist/appJwt.js +3 -12
  34. package/dist/appJwt.js.map +1 -1
  35. package/dist/appTokens.d.ts.map +1 -1
  36. package/dist/appTokens.js +2 -1
  37. package/dist/appTokens.js.map +1 -1
  38. package/dist/artifactErrors.d.ts +34 -0
  39. package/dist/artifactErrors.d.ts.map +1 -0
  40. package/dist/artifactErrors.js +27 -0
  41. package/dist/artifactErrors.js.map +1 -0
  42. package/dist/artifactExtraction.d.ts +12 -0
  43. package/dist/artifactExtraction.d.ts.map +1 -0
  44. package/dist/artifactExtraction.js +40 -0
  45. package/dist/artifactExtraction.js.map +1 -0
  46. package/dist/artifactPaths.d.ts +4 -0
  47. package/dist/artifactPaths.d.ts.map +1 -0
  48. package/dist/artifactPaths.js +35 -0
  49. package/dist/artifactPaths.js.map +1 -0
  50. package/dist/artifacts.d.ts +13 -8
  51. package/dist/artifacts.d.ts.map +1 -1
  52. package/dist/artifacts.js +18 -10
  53. package/dist/artifacts.js.map +1 -1
  54. package/dist/authenticatedRequest.d.ts +5 -5
  55. package/dist/authenticatedRequest.d.ts.map +1 -1
  56. package/dist/authenticatedRequest.js +6 -5
  57. package/dist/authenticatedRequest.js.map +1 -1
  58. package/dist/cache.d.ts.map +1 -1
  59. package/dist/cache.js +2 -1
  60. package/dist/cache.js.map +1 -1
  61. package/dist/checkRuns.d.ts +4 -4
  62. package/dist/checkRuns.d.ts.map +1 -1
  63. package/dist/checkRuns.js +5 -3
  64. package/dist/checkRuns.js.map +1 -1
  65. package/dist/errors.d.ts +8 -0
  66. package/dist/errors.d.ts.map +1 -1
  67. package/dist/errors.js +6 -0
  68. package/dist/errors.js.map +1 -1
  69. package/dist/gitSchemas.d.ts +195 -0
  70. package/dist/gitSchemas.d.ts.map +1 -0
  71. package/dist/gitSchemas.js +45 -0
  72. package/dist/gitSchemas.js.map +1 -0
  73. package/dist/installationClients.d.ts +291 -15
  74. package/dist/installationClients.d.ts.map +1 -1
  75. package/dist/installationClients.js +14 -5
  76. package/dist/installationClients.js.map +1 -1
  77. package/dist/markdown.d.ts +1 -0
  78. package/dist/markdown.d.ts.map +1 -1
  79. package/dist/markdown.js +3 -0
  80. package/dist/markdown.js.map +1 -1
  81. package/dist/markdownRows.d.ts +8 -1
  82. package/dist/markdownRows.d.ts.map +1 -1
  83. package/dist/markdownRows.js +39 -8
  84. package/dist/markdownRows.js.map +1 -1
  85. package/dist/pagedLists.d.ts +15 -0
  86. package/dist/pagedLists.d.ts.map +1 -0
  87. package/dist/pagedLists.js +16 -0
  88. package/dist/pagedLists.js.map +1 -0
  89. package/dist/repositoryAddress.d.ts +11 -0
  90. package/dist/repositoryAddress.d.ts.map +1 -0
  91. package/dist/repositoryAddress.js +23 -0
  92. package/dist/repositoryAddress.js.map +1 -0
  93. package/dist/repositoryReads.d.ts +262 -0
  94. package/dist/repositoryReads.d.ts.map +1 -0
  95. package/dist/repositoryReads.js +30 -0
  96. package/dist/repositoryReads.js.map +1 -0
  97. package/dist/repositorySchemas.d.ts +73 -0
  98. package/dist/repositorySchemas.d.ts.map +1 -0
  99. package/dist/repositorySchemas.js +38 -0
  100. package/dist/repositorySchemas.js.map +1 -0
  101. package/dist/representations.d.ts +5 -5
  102. package/dist/representations.d.ts.map +1 -1
  103. package/dist/representations.js +3 -2
  104. package/dist/representations.js.map +1 -1
  105. package/dist/requests.d.ts +10 -6
  106. package/dist/requests.d.ts.map +1 -1
  107. package/dist/requests.js +11 -6
  108. package/dist/requests.js.map +1 -1
  109. package/dist/status.d.ts +2 -2
  110. package/dist/status.d.ts.map +1 -1
  111. package/dist/status.js +2 -3
  112. package/dist/status.js.map +1 -1
  113. package/dist/stickyComments.d.ts +1 -1
  114. package/dist/verifyWebhook.d.ts +2 -2
  115. package/dist/verifyWebhook.d.ts.map +1 -1
  116. package/dist/verifyWebhook.js +3 -10
  117. package/dist/verifyWebhook.js.map +1 -1
  118. package/dist/viewerRepositoryAccess.d.ts +46 -0
  119. package/dist/viewerRepositoryAccess.d.ts.map +1 -0
  120. package/dist/viewerRepositoryAccess.js +54 -0
  121. package/dist/viewerRepositoryAccess.js.map +1 -0
  122. package/package.json +13 -6
  123. package/src/GitHub.ts +34 -15
  124. package/src/adapters/manualRedirects.ts +6 -0
  125. package/src/adapters/mergeResponseHeaders.ts +8 -0
  126. package/src/adapters/resolveApiUrl.ts +5 -0
  127. package/src/adapters/signAppJwt.ts +16 -0
  128. package/src/adapters/verifySignature.ts +13 -0
  129. package/src/adapters/zipEntries.ts +69 -0
  130. package/src/appJwt.ts +3 -12
  131. package/src/appTokens.ts +2 -1
  132. package/src/artifactErrors.ts +26 -0
  133. package/src/artifactExtraction.ts +62 -0
  134. package/src/artifactPaths.ts +36 -0
  135. package/src/artifacts.ts +35 -40
  136. package/src/authenticatedRequest.ts +8 -9
  137. package/src/cache.ts +2 -1
  138. package/src/checkRuns.ts +5 -5
  139. package/src/errors.ts +6 -0
  140. package/src/gitSchemas.ts +52 -0
  141. package/src/installationClients.ts +18 -12
  142. package/src/markdown.ts +4 -0
  143. package/src/markdownRows.ts +54 -9
  144. package/src/pagedLists.ts +40 -0
  145. package/src/repositoryAddress.ts +32 -0
  146. package/src/repositoryReads.ts +62 -0
  147. package/src/repositorySchemas.ts +43 -0
  148. package/src/representations.ts +6 -5
  149. package/src/requests.ts +12 -14
  150. package/src/status.ts +3 -3
  151. package/src/verifyWebhook.ts +4 -10
  152. package/src/viewerRepositoryAccess.ts +66 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 - 2026-11-03
4
+
5
+ ### Added
6
+
7
+ - Add `details(summary, text)` to render raw text with escaped HTML and Markdown in both the summary and body.
8
+ - Add `renderSections` to share one UTF-8 byte budget across row lists, reserving headers and an `and N more` paragraph for each truncated section.
9
+ - Export `parseRepositoryAddress` from `repositoryAddress.ts`, returning an Effect `Result` with the address form, repository identity, a pull-request number when present, or a typed `GitHubAddressInvalid` failure.
10
+
11
+ ### Fixed
12
+
13
+ - Preserve each pagination pacing callback's failure type through raw, decoded, bounded and repository lists.
14
+ - Keep cached pagination links when GitHub omits them from a 304 response, so repeated reads return every page.
15
+
16
+ ### Breaking
17
+
18
+ - `renderRows` now returns `Result<string, MarkdownBudgetTooSmall>` instead of a string or a thrown `RangeError`. Match the native Effect `Result` with `Result.match`, or convert it with `Effect.fromResult`. Budget failures carry the requested `byteLimit`; import `MarkdownBudgetTooSmall` from `@shivaedev/github/errors.ts`.
19
+ - Initialize the low-level request cache with `yield* requests(rates)` instead of `requests(client, token, rates, refresh)`. Provide `HttpClient`, `GitHubToken` and, when needed, `TokenRefresh` through the Effect environment. The `GitHub` service API is unchanged.
20
+
21
+ ## 0.10.0 - 2026-11-02
22
+
23
+ ### Added
24
+
25
+ - Add typed, byte-capped issue, user, collaborator-permission, commit, comparison and tree reads, plus paged issue, pull-request-file and commit lists on the GitHub service.
26
+ - Let Schema-aware lists select arrays inside envelopes, pace each request and opt into skipping undecodable items. `listPages` reports decoded items and skipped counts for each page; `listBounded` includes the total `skipped` count alongside `items` and `truncated`. Existing numeric byte-limit arguments remain supported, and decoding still fails by default.
27
+ - `GitHubApp.viewerRepositories(account)` accepts a GitHub account ID or login and returns readable and administrable installed repositories, with access results cached for 60 seconds across at most 100 accounts.
28
+
29
+ ### Fixed
30
+
31
+ - Preserve configured GitHub API path prefixes in representation reads, lists and writes, App installation discovery and token exchange, and artifact API requests. API origins without a path keep their existing behavior.
32
+ - Keep shared App clients independent of a single-installation Layer's repository and permission scope.
33
+
34
+ ### Breaking
35
+
36
+ - `downloadArtifactArchive` now returns an array of `{ path, bytes }` files instead of raw ZIP bytes. Install the optional `@zip.js/zip.js` peer and consume these validated files instead of passing the result to an application ZIP reader. Extraction refuses unsafe paths, symlinks, duplicate paths and file/directory conflicts, with distinct typed errors. The default caps are 100 MiB of extracted contents and a per-entry compression ratio of 100:1; set `totalSizeLimit` and `compressionRatioLimit` to change them. The existing `byteLimit` still caps the downloaded ZIP.
37
+ - The low-level `artifacts` constructor now takes `(apiUrl, rates)` and returns an Effect. Yield it with `HttpClient`, `GitHubToken` and `TokenRefresh` provided instead of passing those services as arguments.
38
+
3
39
  ## 0.9.0 - 2026-11-02
4
40
 
5
41
  ### Added
package/README.md CHANGED
@@ -16,6 +16,8 @@ const repository = read("/repos/acme/widgets", Repository);
16
16
 
17
17
  ## Using the client
18
18
 
19
+ `parseRepositoryAddress` from `@shivaedev/github/repositoryAddress.ts` parses repository shorthand (`owner/name` or `github:owner/name`), GitHub HTTP(S), Git, SCP-style SSH and `ssh://git@github.com/` remotes, including `git+` prefixes and `.git` suffixes. It trims surrounding whitespace and returns an Effect `Result` containing `{ kind, owner, name, repository, number }`. `kind` distinguishes `remote`, `shorthand` and `pull-request`, so a Git remote reader can reject shorthand that Git treats as a local path. `repository` is `owner/name`; `number` is undefined for repositories and a positive safe integer for canonical `https://github.com/owner/name/pull/number` addresses. Unsupported hosts, malformed addresses and invalid PR numbers return the string-tagged `GitHubAddressInvalid` error from `@shivaedev/github/errors.ts`. PR subpages, queries and fragments are rejected.
20
+
19
21
  `GitHub` is a native Context service declared with `defineService`. Its Layer owns the conditional response cache and resource observations. Provide an Effect `HttpClient` and a `GitHubToken` once, then use the Schema-aware `read`, `list` and `write` functions. Schema decoding services remain visible in their requirements.
20
22
 
21
23
  ```ts
@@ -76,7 +78,7 @@ const diff = Effect.gen(function* () {
76
78
 
77
79
  The service's `read(path)` and `write(method, path, body)` return a representation with a generic `decode(schema)` method. Its `list(path)` returns an Effect containing a Stream of page representations. These methods accept the same trailing byte limit: `read(path, byteLimit)`, `write(method, path, body, byteLimit)` and `list(path, beforeRequest, byteLimit)`; pass `undefined` for `beforeRequest` when no pacing callback is needed. Prefer the exported functions for Schema-decoded items. Each page representation exposes `hasNext`, which reports whether its Link header names a next page without fetching it. A caller with a pacing policy can pass `github.list(path, beforeRequest)`: the callback receives the resource name before each page request, and its typed failure stops the stream. `github.resource(path)` returns the observed resource for that path, defaulting to core or search before the first response.
78
80
 
79
- Use `listBounded(path, itemSchema, pageCap, byteLimit?)` to collect a prefix within a positive page cap. It returns `{ items, truncated }`; `truncated` is true exactly when the last fetched page names a next page. It never requests or decodes a page beyond the cap, and uses the same pagination and per-page byte limits as `list`.
81
+ Use `listBounded(path, itemSchema, pageCap, options?)` to collect a prefix within a positive page cap. It returns `{ items, skipped, truncated }`; `truncated` is true exactly when the last fetched page names a next page. It never requests or decodes a page beyond the cap, and uses the same pagination and per-page byte limits as `list`.
80
82
 
81
83
  ```ts
82
84
  import { listBounded } from "@shivaedev/github/GitHub.ts";
@@ -84,6 +86,55 @@ import { listBounded } from "@shivaedev/github/GitHub.ts";
84
86
  const issues = listBounded("/repos/acme/widgets/issues", Issue, 2);
85
87
  ```
86
88
 
89
+ ### Enveloped lists and partial decoding
90
+
91
+ `list`, `listPages` and `listBounded` accept either the existing numeric byte cap or a `ListOptions` object (from `@shivaedev/github/pagedLists.ts`) as their last argument. Set `arrayKey` to read an array inside an envelope, such as `check_runs` or `artifacts`. The options also accept `responseByteLimit` and the service's `beforeRequest` pacing callback. Item Schemas retain their decoding service requirements.
92
+
93
+ ```ts
94
+ const checks = listBounded("/repos/acme/widgets/commits/main/check-runs", CheckRun, 2, {
95
+ arrayKey: "check_runs",
96
+ onDecodeError: "skip",
97
+ responseByteLimit: 2 * 1024 * 1024,
98
+ });
99
+ ```
100
+
101
+ Import `CheckRun` from `@shivaedev/github/checkRuns.ts`. Decoding fails at the first invalid item by default. With `onDecodeError: "skip"`, valid items keep their order and `skipped` counts rejected items across the fetched pages. Invalid JSON, a missing envelope key, a non-array value, HTTP failures and byte-limit failures still fail the read. A page containing only rejected items still follows its next Link. The page cap counts fetched pages, including empty pages, and never counts pages beyond the cap.
102
+
103
+ `listPages(path, itemSchema, options?)` returns a Stream of `{ items, skipped, hasNext }` pages for consumers that need streaming counts. `list` flattens those pages into items. Both fetch only while consumed.
104
+
105
+ ### Daily repository reads
106
+
107
+ The `GitHub` service includes Schema-decoded reads for common repository work:
108
+
109
+ | Method | Result |
110
+ | -------------------------------------------------------------- | ------------------------------------------------------------------ |
111
+ | `getIssue(owner, repo, number, byteLimit?)` | `Issue` |
112
+ | `getUser(username, byteLimit?)` | `User` |
113
+ | `getCollaboratorPermission(owner, repo, username, byteLimit?)` | `CollaboratorPermission` |
114
+ | `getCommit(owner, repo, ref, byteLimit?)` | `CommitDetail`, including stats and optional files |
115
+ | `compareCommits(owner, repo, base, head, byteLimit?)` | `Comparison`, including commits, optional files and `totalCommits` |
116
+ | `getTree(owner, repo, ref, options?)` | `GitTree`, including GitHub's `truncated` flag |
117
+ | `listIssues(owner, repo, options?)` | Effect containing a Stream of decoded issue pages |
118
+ | `listPullRequestFiles(owner, repo, number, options?)` | Effect containing a Stream of decoded file pages |
119
+ | `listCommits(owner, repo, options?)` | Effect containing a Stream of decoded commit pages |
120
+
121
+ The Schemas live in `@shivaedev/github/repositorySchemas.ts` and `@shivaedev/github/gitSchemas.ts`. Results use camelCase field names. Commit authors can be null, file patches can be absent for binary files, and renamed files expose `previousFilename`.
122
+
123
+ ```ts
124
+ const review = Effect.gen(function* () {
125
+ const github = yield* GitHub;
126
+ const issue = yield* github.getIssue("acme", "widgets", 3);
127
+ const pages = yield* github.listPullRequestFiles("acme", "widgets", 3, { responseByteLimit: 2 * 1024 * 1024 });
128
+ const files = yield* pages.pipe(
129
+ Stream.flatMap((page) => Stream.fromIterable(page.items)),
130
+ Stream.runCollect,
131
+ );
132
+ return { issue, files };
133
+ });
134
+ ```
135
+
136
+ These reads share the client's authentication, conditional representations and body caps. List options accept `query` for GitHub filters such as issue `state` or commit `sha`, along with the decoding and pacing options above. Lists request `per_page=100` unless overridden in `query`. Tree options accept `recursive` and `responseByteLimit`. Refs containing slashes are encoded as path segments. Compare and single-commit reads return one GitHub response; use `totalCommits` and GitHub's endpoint limits when choosing whether the response is sufficient for your application.
137
+
87
138
  ### Acting as a GitHub App
88
139
 
89
140
  Provide `githubApp` as the `GitHubToken` Layer. With just the App ID and a PEM private key wrapped in `Redacted`, the client signs App JWTs for App-level requests. Add an installation ID for installation requests. Both use the same Schema-aware `read` and paginated `list` functions.
@@ -135,6 +186,24 @@ Provide `app(privateKey)` once around `bothInstallations` or the application. Ea
135
186
 
136
187
  The shared cache holds at most 100 installation clients and evicts the least recently used client when full. Selecting a client renews its 30-minute idle lifetime; a client idle for that interval is replaced on its next selection. Expiry is lazy, with no background timer. Eviction discards the client's token and request state, and its next request exchanges a new token. Keep operations inside `withInstallation` so each selection uses the cache's current client.
137
188
 
189
+ ### Repositories visible to an account
190
+
191
+ Pass the GitHub account from your application's session to `GitHubApp.viewerRepositories(account)`. A number is a GitHub account ID; a string is a login. Provide the same `githubApp(credentials)` Layer as above. The package has no dependency on your session or identity service.
192
+
193
+ ```ts
194
+ import { Effect } from "effect";
195
+ import { GitHubApp } from "@shivaedev/github/installationClients.ts";
196
+
197
+ const access = Effect.gen(function* () {
198
+ const app = yield* GitHubApp;
199
+ return yield* app.viewerRepositories(12345);
200
+ });
201
+ ```
202
+
203
+ The result contains `readable` and `administrable` arrays. Each entry has `id`, `name`, `owner.login`, `htmlUrl`, `private` and `installationId`. Only repositories installed for this App are included, across all installation and repository pages. Public repositories are readable even without a collaborator grant. Private repositories require GitHub's `read`, `triage`, `write`, `maintain` or `admin` permission; only `admin` grants administration. A permission lookup returning 404 means no collaborator grant. Other lookup failures fail the operation instead of returning partial access. The App needs repository Metadata read permission for these lookups.
204
+
205
+ Successful results are cached for 60 seconds, across at most 100 account keys, evicting the least recently used key when full. Login casing shares a key. Reads do not extend the lifetime. After expiry, the next call revalidates installation lists, repository lists and permissions, so revoked access, changed roles and removed repositories remain cached for at most that interval. Failed lookups are immediately eligible for another attempt. These are cached observations of GitHub access; the application chooses its authorization policy.
206
+
138
207
  `scope` accepts `repositoryIds` or `repositories` (repository names), and a `permissions` record of GitHub permission names to `"read"`, `"write"` or `"admin"`. Omit `scope` for the installation's full access. Explicit `{ permissions: { metadata: "read" } }` requests a metadata-only token for access checks. GitHub decides whether the requested scope is allowed.
139
208
 
140
209
  Alternatively, pass `{ appId, privateKey, owner: "acme", repository: "widgets", scope? }`. The Layer resolves the installation through `GET /repos/{owner}/{repo}/installation` before exchanging the App JWT for an installation token. Signing uses Node's built-in crypto. The RS256 JWT carries the App ID as `iss`, `iat` 60 seconds before the current time, and `exp` ten minutes ahead.
@@ -147,26 +216,49 @@ Installation lookups and token exchanges follow the same primary and secondary r
147
216
 
148
217
  Other App authentication failures use `GitHubAppError` from `errors.ts`. Its string `reason` distinguishes `"sign"` (JWT signing), `"encode"` (request body encoding), `"transport"` (sending or reading the response), `"status"` (an HTTP refusal) and `"response"` (invalid or oversized response data). A refused exchange or installation lookup with HTTP 401 has `{ reason: "status", status: 401 }`. Caught failures remain available as a non-enumerable `cause`, excluded from JSON serialization; causes can contain credentials from the underlying HTTP request, so keep them in trusted diagnostics. The serialized errors, logs and spans exclude the private key, App JWT and installation token.
149
218
 
150
- ### Downloading workflow-artifact archives
219
+ ### Downloading and extracting workflow artifacts
220
+
221
+ Install the optional ZIP reader to extract artifacts:
222
+
223
+ ```sh
224
+ pnpm add @zip.js/zip.js
225
+ ```
151
226
 
152
- Call `downloadArtifactArchive` with the artifact ID and a byte cap. It requests GitHub's ZIP endpoint with the current installation token, reads the 302 Location explicitly, and issues a fresh request to storage without the GitHub Authorization or API-version headers. Fetch requests use manual redirects and omit ambient credentials; provide an HTTP client without automatic redirect middleware.
227
+ Call `downloadArtifactArchive` with the artifact ID and a download byte cap. It requests GitHub's ZIP endpoint with the current installation token, reads the 302 Location explicitly, and issues a fresh request to storage without the GitHub Authorization or API-version headers. Fetch requests use manual redirects and omit ambient credentials; provide an HTTP client without automatic redirect middleware.
153
228
 
154
229
  ```ts
155
230
  import { Effect } from "effect";
156
231
  import { GitHub } from "@shivaedev/github/GitHub.ts";
157
232
 
158
- const archive = Effect.gen(function* () {
233
+ const files = Effect.gen(function* () {
159
234
  const github = yield* GitHub;
160
235
  return yield* github.downloadArtifactArchive({
161
236
  owner: "acme",
162
237
  repo: "widgets",
163
238
  artifactId: 7,
164
239
  byteLimit: 10 * 1024 * 1024,
240
+ totalSizeLimit: 100 * 1024 * 1024,
241
+ compressionRatioLimit: 100,
165
242
  });
166
243
  });
167
244
  ```
168
245
 
169
- Provide an installation client such as `clients(privateKey).background` from the GitHub App example above. The result is a `Uint8Array` of archive bytes. The package counts bytes while streaming, independently of transport limits, and stops on overflow with `GitHubArtifactTooLarge({ limit })` from `errors.ts`. This replaces application-owned redirect handling and archive body limits. It does not extract the ZIP.
246
+ Provide an installation client such as `clients(privateKey).background` from the GitHub App example above. The result is an array of `{ path, bytes }` files (`ArtifactFile` from `artifactExtraction.ts`), with relative POSIX paths and `Uint8Array` contents. Directory entries are validated and omitted from the result. Extraction happens in memory and writes no files. The ZIP reader loads on demand; other GitHub operations do not need it.
247
+
248
+ `byteLimit` caps the downloaded ZIP while streaming and fails with `GitHubArtifactTooLarge({ limit })` from `errors.ts`. Extraction defaults to a `totalSizeLimit` of 100 MiB across all entries and a per-entry `compressionRatioLimit` of 100:1. Both caps are inclusive. The total limit must be a non-negative safe integer; the ratio must be finite and positive. Limits check declared sizes before decompression and emitted bytes before retaining them. No partial file list is returned after a refusal.
249
+
250
+ Extraction failures come from `artifactErrors.ts`:
251
+
252
+ | Error | Refusal |
253
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
254
+ | `GitHubArtifactPathTraversal({ path })` | Absolute paths, drive paths, backslashes, colons, NUL, empty or dot path components, and parent traversal |
255
+ | `GitHubArtifactSymlink({ path })` | Symbolic links |
256
+ | `GitHubArtifactDuplicateEntry({ path })` | Duplicate paths and file/directory conflicts |
257
+ | `GitHubArtifactCompressionRatio({ path, limit })` | An entry exceeds the ratio cap |
258
+ | `GitHubArtifactExtractedTooLarge({ limit })` | Combined extracted contents exceed the total cap |
259
+ | `GitHubArtifactExtractionError({ reason })` | Missing reader (`"reader-unavailable"`), malformed ZIP (`"invalid-archive"`) or invalid caps (`"invalid-limits"`) |
260
+
261
+ Paths remain case-sensitive.
170
262
 
171
263
  A missing (404) or expired (410) artifact fails with `GitHubArtifactError({ stage: "redirect", status })`. A storage HTTP failure uses the same error with `stage: "download"`. Transport and GitHub rate-limit failures retain their usual typed errors.
172
264
 
@@ -261,6 +353,8 @@ const check = (headSha: string) =>
261
353
 
262
354
  Use the pure helpers from `markdown.ts` for test names and error messages. `plainText` encodes ASCII punctuation as HTML entities, including HTML delimiters and link syntax, and adds a zero-width separator after `@` and `#` to prevent mentions and issue references. `codeSpan` chooses a backtick fence longer than any run in the input and flattens line endings to spaces. `tableCell` flattens line endings and protects pipes; pass `"code"` for a code span inside a table cell. Emoji are preserved.
263
355
 
356
+ `details(summary, text)` wraps raw text in `<details>` and `<summary>`, escaping both inputs with `plainText`. Closing tags, HTML and Markdown in either input remain literal text. Pass raw text rather than rendered Markdown; body line endings and Unicode are preserved.
357
+
264
358
  ```ts
265
359
  import { tableCell } from "@shivaedev/github/markdown.ts";
266
360
  import { renderRows } from "@shivaedev/github/markdownRows.ts";
@@ -272,9 +366,23 @@ const summary = renderRows(rows, {
272
366
  });
273
367
  ```
274
368
 
275
- `renderRows` accepts already-rendered rows and a trusted Markdown header. It counts UTF-8 bytes, keeps a prefix of complete rows and, when rows are dropped, appends one `and N more` paragraph. The header and notice count toward the budget; rows, emoji and escapes are never sliced. The default budget is 60,000 bytes. A budget too small for the header and notice throws `RangeError`.
369
+ `renderRows` accepts already-rendered rows and a trusted Markdown header. It counts UTF-8 bytes, keeps a prefix of complete rows and, when rows are dropped, appends one `and N more` paragraph. The header and notice count toward the budget; rows, emoji and escapes are never sliced. The default budget is 60,000 bytes. It returns `Result<string, MarkdownBudgetTooSmall>`; a budget too small for the header and notice is a typed failure. `MarkdownBudgetTooSmall` is exported from `errors.ts` and carries the requested `byteLimit`.
370
+
371
+ For several lists in one comment, `renderSections` takes sections with `rows` and an optional trusted Markdown `header`, plus one `{ byteLimit }` option for the whole result. It joins sections with blank lines and counts those separators, headers and notices in the shared budget, defaulting to 60,000 bytes. Earlier sections receive spare space first while later sections retain room for their headers and either all their rows or an `and N more` paragraph. Each truncated section has its own omitted-row count. Empty sections without headers are omitted; header-only sections remain. If the total budget cannot fit the required headers and notices or complete shorter lists, it returns a `MarkdownBudgetTooSmall` failure carrying the total requested `byteLimit`.
372
+
373
+ ```ts
374
+ import { renderSections } from "@shivaedev/github/markdownRows.ts";
375
+
376
+ const report = renderSections(
377
+ [
378
+ { header: "## Failures", rows: failureRows },
379
+ { header: "## Warnings", rows: warningRows },
380
+ ],
381
+ { byteLimit: 8_000 },
382
+ );
383
+ ```
276
384
 
277
- Use the result as a sticky comment body or check-run summary. The budget covers the rendered body; account for any surrounding text, including the sticky comment marker, when choosing a lower total limit.
385
+ Both row helpers return native Effect `Result` values. Match them with `Result.match`, or convert them with `Effect.fromResult` to pass budget failures through the error channel. Use the successful string as a sticky comment body or check-run summary. The budget covers the rendered body; account for any surrounding text, including the sticky comment marker, when choosing a lower total limit.
278
386
 
279
387
  ### Rate limits and failures
280
388