@shivaedev/github 0.0.0 → 0.5.1
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.
- package/LICENSE +21 -0
- package/README.md +238 -1
- package/dist/GitHub.d.ts +93 -0
- package/dist/GitHub.d.ts.map +1 -0
- package/dist/GitHub.js +53 -0
- package/dist/GitHub.js.map +1 -0
- package/dist/GitHubConfig.d.ts +6 -0
- package/dist/GitHubConfig.d.ts.map +1 -0
- package/dist/GitHubConfig.js +5 -0
- package/dist/GitHubConfig.js.map +1 -0
- package/dist/GitHubToken.d.ts +9 -0
- package/dist/GitHubToken.d.ts.map +1 -0
- package/dist/GitHubToken.js +8 -0
- package/dist/GitHubToken.js.map +1 -0
- package/dist/TokenRefresh.d.ts +5 -0
- package/dist/TokenRefresh.d.ts.map +1 -0
- package/dist/TokenRefresh.js +5 -0
- package/dist/TokenRefresh.js.map +1 -0
- package/dist/appJwt.d.ts +8 -0
- package/dist/appJwt.d.ts.map +1 -0
- package/dist/appJwt.js +16 -0
- package/dist/appJwt.js.map +1 -0
- package/dist/artifacts.d.ts +16 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +31 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/authenticatedRequest.d.ts +8 -0
- package/dist/authenticatedRequest.d.ts.map +1 -0
- package/dist/authenticatedRequest.js +42 -0
- package/dist/authenticatedRequest.js.map +1 -0
- package/dist/cache.d.ts +12 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +17 -0
- package/dist/cache.js.map +1 -0
- package/dist/checkRuns.d.ts +104 -0
- package/dist/checkRuns.d.ts.map +1 -0
- package/dist/checkRuns.js +42 -0
- package/dist/checkRuns.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/githubApp.d.ts +24 -0
- package/dist/githubApp.d.ts.map +1 -0
- package/dist/githubApp.js +55 -0
- package/dist/githubApp.js.map +1 -0
- package/dist/json.d.ts +7 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +14 -0
- package/dist/json.js.map +1 -0
- package/dist/markdown.d.ts +4 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +24 -0
- package/dist/markdown.js.map +1 -0
- package/dist/markdownRows.d.ts +6 -0
- package/dist/markdownRows.d.ts.map +1 -0
- package/dist/markdownRows.js +26 -0
- package/dist/markdownRows.js.map +1 -0
- package/dist/rateLimits.d.ts +20 -0
- package/dist/rateLimits.d.ts.map +1 -0
- package/dist/rateLimits.js +43 -0
- package/dist/rateLimits.js.map +1 -0
- package/dist/representations.d.ts +18 -0
- package/dist/representations.d.ts.map +1 -0
- package/dist/representations.js +28 -0
- package/dist/representations.js.map +1 -0
- package/dist/requests.d.ts +10 -0
- package/dist/requests.d.ts.map +1 -0
- package/dist/requests.js +34 -0
- package/dist/requests.js.map +1 -0
- package/dist/status.d.ts +7 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +57 -0
- package/dist/status.js.map +1 -0
- package/dist/stickyComments.d.ts +46 -0
- package/dist/stickyComments.d.ts.map +1 -0
- package/dist/stickyComments.js +37 -0
- package/dist/stickyComments.js.map +1 -0
- package/dist/verifyWebhook.d.ts +4 -0
- package/dist/verifyWebhook.d.ts.map +1 -0
- package/dist/verifyWebhook.js +14 -0
- package/dist/verifyWebhook.js.map +1 -0
- package/package.json +71 -4
- package/src/GitHub.ts +68 -0
- package/src/GitHubConfig.ts +5 -0
- package/src/GitHubToken.ts +16 -0
- package/src/TokenRefresh.ts +7 -0
- package/src/appJwt.ts +21 -0
- package/src/artifacts.ts +57 -0
- package/src/authenticatedRequest.ts +55 -0
- package/src/cache.ts +25 -0
- package/src/checkRuns.ts +48 -0
- package/src/errors.ts +34 -0
- package/src/githubApp.ts +87 -0
- package/src/json.ts +16 -0
- package/src/markdown.ts +25 -0
- package/src/markdownRows.ts +30 -0
- package/src/rateLimits.ts +56 -0
- package/src/representations.ts +57 -0
- package/src/requests.ts +44 -0
- package/src/status.ts +64 -0
- package/src/stickyComments.ts +49 -0
- package/src/verifyWebhook.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShivaeDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,240 @@
|
|
|
1
1
|
# @shivaedev/github
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GitHub integrations should share one client instead of repeating token headers, page loops and quota handling in every application. This package turns GitHub REST responses into Schema-decoded Effects and streams while keeping rate limits typed.
|
|
4
|
+
|
|
5
|
+
## Read GitHub with less application code
|
|
6
|
+
|
|
7
|
+
Describe the response you need and read it. Unchanged responses reuse their decoded value after ETag revalidation; paged lists fetch as you consume them.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Schema } from "effect";
|
|
11
|
+
import { read } from "@shivaedev/github/GitHub.ts";
|
|
12
|
+
|
|
13
|
+
const Repository = Schema.Struct({ name: Schema.String });
|
|
14
|
+
const repository = read("/repos/acme/widgets", Repository);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Using the client
|
|
18
|
+
|
|
19
|
+
`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
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Effect, Layer, Schema, Stream } from "effect";
|
|
23
|
+
import { FetchHttpClient } from "effect/http";
|
|
24
|
+
import { GitHub, list, read, write } from "@shivaedev/github/GitHub.ts";
|
|
25
|
+
import { GitHubToken } from "@shivaedev/github/GitHubToken.ts";
|
|
26
|
+
|
|
27
|
+
const github = GitHub.layer.pipe(
|
|
28
|
+
Layer.provide(FetchHttpClient.layer),
|
|
29
|
+
Layer.provide(GitHubToken.fromEnv),
|
|
30
|
+
);
|
|
31
|
+
const Issue = Schema.Struct({ number: Schema.Number, title: Schema.String });
|
|
32
|
+
const program = Effect.gen(function* () {
|
|
33
|
+
const issue = yield* read("/repos/acme/widgets/issues/1", Issue);
|
|
34
|
+
const first = yield* list("/repos/acme/widgets/issues", Issue).pipe(
|
|
35
|
+
Stream.take(1),
|
|
36
|
+
Stream.runCollect,
|
|
37
|
+
);
|
|
38
|
+
const created = yield* write("POST", "/repos/acme/widgets/issues", { title: "A new issue" }, Issue);
|
|
39
|
+
return { issue, first, created };
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await Effect.runPromise(program.pipe(Effect.provide(github)));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`GitHubToken.fromEnv` reads `GH_TOKEN`, then `GITHUB_TOKEN`, through Effect Config. If neither is set, requests are unauthenticated. To supply a token directly, provide `GitHubToken` with `Option.some(Redacted.make(token))`; use `Option.none()` for anonymous access. Requests send `Accept: application/vnd.github+json` and `X-GitHub-Api-Version: 2022-11-28`.
|
|
46
|
+
|
|
47
|
+
The default base URL is `https://api.github.com`. Provide `GitHubConfig` from `@shivaedev/github/GitHubConfig.ts` with `{ apiUrl }` for another GitHub API endpoint.
|
|
48
|
+
|
|
49
|
+
`read(path, schema)` decodes a JSON response. `list(path, itemSchema)` returns a Stream of decoded items across GitHub's Link pages, and stops fetching when consumption stops. `write(method, path, body, schema)` sends JSON with `POST` or `PATCH` and decodes its response. JSON response bodies default to a 1 MiB cap. Set `GitHubConfig.responseByteLimit` once for a client or pass a trailing byte-limit argument to these helpers for one request. The cap applies to each list page and is enforced while reading, before Schema decoding; exceeding it fails with typed `ResponseBodyTooLarge` without buffering the complete body. The conditional cache holds up to 1000 representations and keeps decoded values separate for each Schema.
|
|
50
|
+
|
|
51
|
+
For large comparisons and listings, allow up to 16 MiB through the same bounded, Schema-decoded path instead of maintaining a separate fetch, body reader and decoder in the application.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { GitHubConfig } from "@shivaedev/github/GitHubConfig.ts";
|
|
55
|
+
|
|
56
|
+
const largeGithub = GitHub.layer.pipe(
|
|
57
|
+
Layer.provide(FetchHttpClient.layer),
|
|
58
|
+
Layer.provide(GitHubToken.fromEnv),
|
|
59
|
+
Layer.provide(Layer.succeed(GitHubConfig)({
|
|
60
|
+
apiUrl: "https://api.github.com",
|
|
61
|
+
responseByteLimit: 16 * 1024 * 1024,
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
const Comparison = Schema.Struct({ total_commits: Schema.Number });
|
|
65
|
+
const comparison = read("/repos/acme/widgets/compare/main...feature", Comparison, 16 * 1024 * 1024);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`largeGithub` raises the client default; the final argument on `comparison` overrides the cap for that request, including when using a client with the default cap. A per-request cap can also lower the client default.
|
|
69
|
+
|
|
70
|
+
For diff text and other raw bodies, obtain the service and call `text(path, accept, byteLimit)` or `bytes(path, accept, byteLimit)`. Both bound the body before returning it and use your chosen media type.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const diff = Effect.gen(function* () {
|
|
74
|
+
const github = yield* GitHub;
|
|
75
|
+
return yield* github.text("/repos/acme/widgets/pulls/1", "application/vnd.github.diff", 256 * 1024);
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
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. 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.
|
|
80
|
+
|
|
81
|
+
### Acting as a GitHub App
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Layer, Redacted, Schema, Stream } from "effect";
|
|
87
|
+
import { FetchHttpClient } from "effect/http";
|
|
88
|
+
import { GitHub, list, read } from "@shivaedev/github/GitHub.ts";
|
|
89
|
+
import { githubApp } from "@shivaedev/github/githubApp.ts";
|
|
90
|
+
|
|
91
|
+
const clients = (privateKey: Redacted.Redacted<string>) => {
|
|
92
|
+
const credentials = { appId: "123456", privateKey };
|
|
93
|
+
const client = (auth: ReturnType<typeof githubApp>) => GitHub.layer.pipe(
|
|
94
|
+
Layer.provide(auth),
|
|
95
|
+
Layer.provide(FetchHttpClient.layer),
|
|
96
|
+
);
|
|
97
|
+
return {
|
|
98
|
+
app: client(githubApp(credentials)),
|
|
99
|
+
background: client(githubApp({ ...credentials, installationId: 7 })),
|
|
100
|
+
accessCheck: client(githubApp({
|
|
101
|
+
...credentials, installationId: 7,
|
|
102
|
+
scope: { repositoryIds: [42], permissions: { metadata: "read" } },
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
const appInfo = read("/app", Schema.Struct({ id: Schema.Number, slug: Schema.String }));
|
|
107
|
+
const installations = list("/app/installations", Schema.Struct({ id: Schema.Number })).pipe(Stream.runCollect);
|
|
108
|
+
const repository = read("/repos/acme/widgets", Schema.Struct({ id: Schema.Number, name: Schema.String }));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Provide `clients(privateKey).app` to `appInfo` or `installations`, and `.accessCheck` to `repository`, using `Effect.provide`. The `.background` client receives a full installation token. These Layers replace application-written JWT signing, installation-token exchanges, expiry timers and the resource-401 refresh wrapper; provide the client Layer once around the application or job for each installation and scope to share its token cache.
|
|
112
|
+
|
|
113
|
+
`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.
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
`GitHubToken` accepts a static Option or a lazy Effect returning one; the client resolves it on each request. Code that reads the service directly must evaluate an Effect value before inspecting the Option, using `Effect.isEffect(value) ? yield* value : value`. Custom injection keeps the caller's token lifecycle.
|
|
118
|
+
|
|
119
|
+
An installation token is fetched on the first request and reused until five minutes before `expires_at`, using Effect's Clock. Concurrent requests through the same Layer share the in-flight exchange, including at refresh. A resource 401 invalidates only the rejected cached token and retries once with a newly exchanged token; simultaneous 401s share that exchange. A second 401 fails with typed `RemoteUnauthorized`. App-level JWTs and custom tokens are not automatically retried.
|
|
120
|
+
|
|
121
|
+
A rejected exchange fails with `GitHubAppError` from `errors.ts`; a 401 has `{ stage: "exchange", status: 401 }`. Signing failures have `stage: "sign"`. These errors and their logs and spans contain no private key, App JWT or installation token.
|
|
122
|
+
|
|
123
|
+
### Downloading workflow-artifact archives
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Effect } from "effect";
|
|
129
|
+
import { GitHub } from "@shivaedev/github/GitHub.ts";
|
|
130
|
+
|
|
131
|
+
const archive = Effect.gen(function* () {
|
|
132
|
+
const github = yield* GitHub;
|
|
133
|
+
return yield* github.downloadArtifactArchive({
|
|
134
|
+
owner: "acme", repo: "widgets", artifactId: 7,
|
|
135
|
+
byteLimit: 10 * 1024 * 1024,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Provide the same `appClient(privateKey)` Layer shown 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.
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
### Verifying webhooks
|
|
145
|
+
|
|
146
|
+
Pass the raw request bytes and the `X-Hub-Signature-256` header to `verifyWebhook` before parsing the body. It checks HMAC SHA-256 with a constant-time comparison, then decodes JSON with your Schema. A missing header, tampered body or wrong secret fails with `GitHubWebhookSignatureError` before Schema decoding.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Redacted, Schema } from "effect";
|
|
150
|
+
import { verifyWebhook } from "@shivaedev/github/verifyWebhook.ts";
|
|
151
|
+
|
|
152
|
+
const WorkflowRun = Schema.Struct({
|
|
153
|
+
action: Schema.String,
|
|
154
|
+
workflowRun: Schema.Struct({ id: Schema.Number, conclusion: Schema.String }),
|
|
155
|
+
}).pipe(Schema.encodeKeys({ workflowRun: "workflow_run" }));
|
|
156
|
+
const receiveWorkflowRun = (
|
|
157
|
+
rawBody: Uint8Array,
|
|
158
|
+
signature: string | undefined,
|
|
159
|
+
secret: Redacted.Redacted<string>,
|
|
160
|
+
) => verifyWebhook(rawBody, signature, secret, WorkflowRun);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This decodes a signed `workflow_run` delivery with the fields the application needs. Keep the body bytes as received: parsing and reserializing JSON changes what was signed.
|
|
164
|
+
|
|
165
|
+
### Sticky comments and check runs
|
|
166
|
+
|
|
167
|
+
Use the service's `upsertStickyComment` to keep a report in one pull-request comment. Pass a marker name; the method prefixes the body with `<!-- name -->`, searches comment pages, and edits the first match. It creates a comment when no match exists. By default the marker can appear anywhere in the existing body. An optional `author` login restricts matching to that author's comments, leaving another author's report alone.
|
|
168
|
+
|
|
169
|
+
For a GitHub App, pass its numeric `appId`. Matching then requires the body to start with the marker, `performed_via_github_app.id` to equal that App ID, and `user.type` to be `"Bot"`. This replaces an application-owned comment scan and ownership predicate. The optional `author` login further restricts matching when supplied.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const report = Effect.gen(function* () {
|
|
173
|
+
const github = yield* GitHub;
|
|
174
|
+
return yield* github.upsertStickyComment({
|
|
175
|
+
owner: "acme", repo: "widgets", number: 1,
|
|
176
|
+
marker: "coverage", appId: 123456,
|
|
177
|
+
body: "Coverage: 90%",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`createCheckRun(owner, repo, input)` creates a check for `headSha` with a name and status. `updateCheckRun(owner, repo, id, input)` moves it to `in_progress` or `completed`; completion requires a conclusion. Output contains `title`, `summary` and optional `text`. Request and response Schemas are exported from `checkRuns.ts`.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const check = (headSha: string) => Effect.gen(function* () {
|
|
186
|
+
const github = yield* GitHub;
|
|
187
|
+
const created = yield* github.createCheckRun("acme", "widgets", {
|
|
188
|
+
headSha, name: "Coverage", status: "queued",
|
|
189
|
+
});
|
|
190
|
+
yield* github.updateCheckRun("acme", "widgets", created.id, {
|
|
191
|
+
status: "in_progress", output: { title: "Coverage", summary: "Measuring" },
|
|
192
|
+
});
|
|
193
|
+
return yield* github.updateCheckRun("acme", "widgets", created.id, {
|
|
194
|
+
status: "completed", conclusion: "success",
|
|
195
|
+
output: { title: "Coverage", summary: "90% covered", text: "All targets measured" },
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Rendering untrusted text
|
|
201
|
+
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { tableCell } from "@shivaedev/github/markdown.ts";
|
|
206
|
+
import { renderRows } from "@shivaedev/github/markdownRows.ts";
|
|
207
|
+
|
|
208
|
+
const rows = failures.map(({ name, error }) =>
|
|
209
|
+
`| ${tableCell(name)} | ${tableCell(error, "code")} |`,
|
|
210
|
+
);
|
|
211
|
+
const summary = renderRows(rows, {
|
|
212
|
+
header: "| Test | Error |\n| --- | --- |",
|
|
213
|
+
byteLimit: 8_000,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`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`.
|
|
218
|
+
|
|
219
|
+
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.
|
|
220
|
+
|
|
221
|
+
### Rate limits and failures
|
|
222
|
+
|
|
223
|
+
`github.observations()` returns the latest `resource`, `limit`, `remaining`, `used` and UTC `reset` from response headers, including conditional and failed responses. Core and search budgets remain separate. A primary or secondary rate limit fails with `GitHubRateLimited` from `@shivaedev/github/errors.ts`; its `retryAt` is a UTC `DateTime` usable directly by a job scheduler. Requests to a blocked resource fail locally until that instant. Each rate-limited failure records a log and span event with the request path and budget, excluding credentials and query strings.
|
|
224
|
+
|
|
225
|
+
A 403 with `"Resource not accessible by integration"` fails with `GitHubPermissionDenied`; a locked-conversation 403 fails with `GitHubConversationLocked`. Both come from `@shivaedev/github/errors.ts` and carry the request `path`. Rate-limit detection takes precedence. Other 403s remain `RemoteHttpError`. Applications can handle these tags instead of parsing GitHub error messages:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const publishReport = report.pipe(
|
|
229
|
+
Effect.catchTag("GitHubPermissionDenied", ({ path }) =>
|
|
230
|
+
Effect.succeed({ published: false, reason: "permissions", path }),
|
|
231
|
+
),
|
|
232
|
+
Effect.catchTag("GitHubConversationLocked", ({ path }) =>
|
|
233
|
+
Effect.succeed({ published: false, reason: "locked", path }),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Other HTTP status failures use the typed `RemoteNotFound`, `RemoteUnauthorized` and other errors from `@shivaedev/http/errors.ts`. An oversized body fails with `ResponseBodyTooLarge`; invalid response data fails with `Schema.SchemaError`. Transport failures use `GitHubTransportError`, which contains only the request path.
|
|
239
|
+
|
|
240
|
+
Install with `pnpm add @shivaedev/github effect`. Choose and provide an Effect HTTP client appropriate to your runtime. Scheduling, retries beyond the single installation-token refresh, and application policy stay with the caller.
|
package/dist/GitHub.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import { GitHubToken } from "#GitHubToken.ts";
|
|
4
|
+
export declare const GitHub: import("effect/Context").Service<"@shivaedev/github/GitHub", Readonly<{
|
|
5
|
+
readonly downloadArtifactArchive: (options: import("#artifacts.ts").ArtifactArchiveOptions) => Effect.Effect<Uint8Array<ArrayBuffer>, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubArtifactError | import("./errors.ts").GitHubArtifactTooLarge | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("@shivaedev/http/errors.ts").RemoteHttpError | import("@shivaedev/http/errors.ts").RemoteRateLimited | import("@shivaedev/http/errors.ts").RemoteUnauthorized | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge, never>;
|
|
6
|
+
readonly list: (path: string, beforeRequest?: ((resource: string) => Effect.Effect<void, import("@shivaedev/errors/taxonomy.ts").ServiceUnavailable>) | undefined, byteLimit?: number | undefined) => Effect.Effect<Stream.Stream<{
|
|
7
|
+
decode: <TSchema extends Schema.Constraint>(schema: TSchema) => Effect.Effect<TSchema["Type"], Schema.SchemaError, TSchema["DecodingServices"]>;
|
|
8
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/Cause").NoSuchElementError | import("@shivaedev/http/errors.ts").PaginationLinkError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | import("@shivaedev/errors/taxonomy.ts").ServiceUnavailable | import("@shivaedev/http/errors.ts").RemoteStatusError, never>, never, never>;
|
|
9
|
+
readonly read: (path: string, byteLimit?: number | undefined) => Effect.Effect<{
|
|
10
|
+
decode: <TSchema extends Schema.Constraint>(schema: TSchema) => Effect.Effect<TSchema["Type"], Schema.SchemaError, TSchema["DecodingServices"]>;
|
|
11
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
12
|
+
readonly write: (method: "PATCH" | "POST", path: string, body: unknown, byteLimit?: number | undefined) => Effect.Effect<{
|
|
13
|
+
decode: <TSchema extends Schema.Constraint>(schema: TSchema) => Effect.Effect<TSchema["Type"], Schema.SchemaError, TSchema["DecodingServices"]>;
|
|
14
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/http/HttpBody").HttpBodyError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
15
|
+
readonly createCheckRun: (owner: string, repo: string, input: {
|
|
16
|
+
readonly conclusion?: "action_required" | "cancelled" | "failure" | "neutral" | "skipped" | "stale" | "success" | "timed_out" | undefined;
|
|
17
|
+
readonly headSha: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly output?: {
|
|
20
|
+
readonly summary: string;
|
|
21
|
+
readonly text?: string | undefined;
|
|
22
|
+
readonly title: string;
|
|
23
|
+
} | undefined;
|
|
24
|
+
readonly status: "completed" | "in_progress" | "queued";
|
|
25
|
+
}) => Effect.Effect<{
|
|
26
|
+
readonly conclusion: "action_required" | "cancelled" | "failure" | "neutral" | "skipped" | "stale" | "success" | "timed_out" | null;
|
|
27
|
+
readonly headSha: string;
|
|
28
|
+
readonly id: number;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly output: {
|
|
31
|
+
readonly summary: string | null;
|
|
32
|
+
readonly text: string | null;
|
|
33
|
+
readonly title: string | null;
|
|
34
|
+
};
|
|
35
|
+
readonly status: "completed" | "in_progress" | "queued";
|
|
36
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/http/HttpBody").HttpBodyError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
37
|
+
readonly updateCheckRun: (owner: string, repo: string, id: number, input: {
|
|
38
|
+
readonly output?: {
|
|
39
|
+
readonly summary: string;
|
|
40
|
+
readonly text?: string | undefined;
|
|
41
|
+
readonly title: string;
|
|
42
|
+
} | undefined;
|
|
43
|
+
readonly status: "in_progress" | "queued";
|
|
44
|
+
} | {
|
|
45
|
+
readonly conclusion: "action_required" | "cancelled" | "failure" | "neutral" | "skipped" | "stale" | "success" | "timed_out";
|
|
46
|
+
readonly output?: {
|
|
47
|
+
readonly summary: string;
|
|
48
|
+
readonly text?: string | undefined;
|
|
49
|
+
readonly title: string;
|
|
50
|
+
} | undefined;
|
|
51
|
+
readonly status: "completed";
|
|
52
|
+
}) => Effect.Effect<{
|
|
53
|
+
readonly conclusion: "action_required" | "cancelled" | "failure" | "neutral" | "skipped" | "stale" | "success" | "timed_out" | null;
|
|
54
|
+
readonly headSha: string;
|
|
55
|
+
readonly id: number;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly output: {
|
|
58
|
+
readonly summary: string | null;
|
|
59
|
+
readonly text: string | null;
|
|
60
|
+
readonly title: string | null;
|
|
61
|
+
};
|
|
62
|
+
readonly status: "completed" | "in_progress" | "queued";
|
|
63
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/http/HttpBody").HttpBodyError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
64
|
+
readonly upsertStickyComment: (input: {
|
|
65
|
+
readonly appId?: number | undefined;
|
|
66
|
+
readonly author?: string | undefined;
|
|
67
|
+
readonly body: string;
|
|
68
|
+
readonly marker: string;
|
|
69
|
+
readonly number: number;
|
|
70
|
+
readonly owner: string;
|
|
71
|
+
readonly repo: string;
|
|
72
|
+
}) => Effect.Effect<{
|
|
73
|
+
readonly body: string;
|
|
74
|
+
readonly id: number;
|
|
75
|
+
readonly performedViaGithubApp?: {
|
|
76
|
+
readonly id: number;
|
|
77
|
+
} | null | undefined;
|
|
78
|
+
readonly user: {
|
|
79
|
+
readonly login: string;
|
|
80
|
+
readonly type?: string | undefined;
|
|
81
|
+
} | null;
|
|
82
|
+
}, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/http/HttpBody").HttpBodyError | import("effect/Cause").NoSuchElementError | import("@shivaedev/http/errors.ts").PaginationLinkError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/errors/taxonomy.ts").ServiceUnavailable | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
83
|
+
readonly bytes: (path: string, accept: string, limit: number) => Effect.Effect<Uint8Array<ArrayBufferLike>, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
84
|
+
readonly observations: () => Effect.Effect<import("#rateLimits.ts").RateLimitObservation[], never, never>;
|
|
85
|
+
readonly resource: (path: string) => Effect.Effect<string, never, never>;
|
|
86
|
+
readonly text: (path: string, accept: string, limit: number) => Effect.Effect<string, import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | import("@shivaedev/http/errors.ts").RemoteStatusError, never>;
|
|
87
|
+
}>> & {
|
|
88
|
+
readonly layer: import("effect/Layer").Layer<"@shivaedev/github/GitHub", never, GitHubToken | HttpClient.HttpClient>;
|
|
89
|
+
};
|
|
90
|
+
export declare function read<TSchema extends Schema.Constraint>(path: string, schema: TSchema, responseByteLimit?: number): Effect.Effect<TSchema["Type"], import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/http/errors.ts").RemoteStatusError, "@shivaedev/github/GitHub" | TSchema["DecodingServices"]>;
|
|
91
|
+
export declare function list<TSchema extends Schema.Constraint>(path: string, schema: TSchema, responseByteLimit?: number): Stream.Stream<TSchema["Type"], import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/Cause").NoSuchElementError | import("@shivaedev/http/errors.ts").PaginationLinkError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/errors/taxonomy.ts").ServiceUnavailable | import("@shivaedev/http/errors.ts").RemoteStatusError, "@shivaedev/github/GitHub" | TSchema["DecodingServices"]>;
|
|
92
|
+
export declare function write<TSchema extends Schema.Constraint>(method: "POST" | "PATCH", path: string, body: unknown, schema: TSchema, responseByteLimit?: number): Effect.Effect<TSchema["Type"], import("./errors.ts").GitHubAppError | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | import("./errors.ts").GitHubTransportError | import("effect/http/HttpBody").HttpBodyError | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge | Schema.SchemaError | import("@shivaedev/http/errors.ts").RemoteStatusError, "@shivaedev/github/GitHub" | TSchema["DecodingServices"]>;
|
|
93
|
+
//# sourceMappingURL=GitHub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHub.d.ts","sourceRoot":"","sources":["../src/GitHub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAO9C,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BjB,CAAC;AAEH,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC,EAAE,MAAM,icAEhH;AAED,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC,EAAE,MAAM,omBAKhH;AAED,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,UAAU,EACtD,MAAM,EAAE,MAAM,GAAG,OAAO,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,OAAO,EACf,iBAAiB,CAAC,EAAE,MAAM,gfAK1B"}
|
package/dist/GitHub.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
|
+
import { artifacts } from "#artifacts.ts";
|
|
5
|
+
import { checkRuns } from "#checkRuns.ts";
|
|
6
|
+
import { GitHubConfig } from "#GitHubConfig.ts";
|
|
7
|
+
import { GitHubToken } from "#GitHubToken.ts";
|
|
8
|
+
import { rateLimits } from "#rateLimits.ts";
|
|
9
|
+
import { representations } from "#representations.ts";
|
|
10
|
+
import { requests } from "#requests.ts";
|
|
11
|
+
import { stickyComments } from "#stickyComments.ts";
|
|
12
|
+
import { TokenRefresh } from "#TokenRefresh.ts";
|
|
13
|
+
export const GitHub = defineService({
|
|
14
|
+
id: "@shivaedev/github/GitHub",
|
|
15
|
+
initialize: Effect.gen(function* () {
|
|
16
|
+
const config = yield* GitHubConfig;
|
|
17
|
+
const rates = rateLimits();
|
|
18
|
+
const client = yield* HttpClient.HttpClient;
|
|
19
|
+
const token = yield* GitHubToken;
|
|
20
|
+
const refresh = yield* TokenRefresh;
|
|
21
|
+
return {
|
|
22
|
+
apiUrl: config.apiUrl,
|
|
23
|
+
archive: artifacts(config.apiUrl, client, token, rates, refresh),
|
|
24
|
+
rates,
|
|
25
|
+
request: requests(client, token, rates, refresh),
|
|
26
|
+
responseByteLimit: config.responseByteLimit,
|
|
27
|
+
};
|
|
28
|
+
}),
|
|
29
|
+
methods: ({ apiUrl, archive, rates, request, responseByteLimit }) => {
|
|
30
|
+
const { get, ...client } = representations(apiUrl, request, rates.resource, responseByteLimit);
|
|
31
|
+
return {
|
|
32
|
+
bytes: (path, accept, limit) => Effect.map(request(get(path, accept), limit), (entry) => entry.bytes),
|
|
33
|
+
...client,
|
|
34
|
+
...archive,
|
|
35
|
+
...stickyComments(client),
|
|
36
|
+
...checkRuns(client),
|
|
37
|
+
observations: () => Effect.sync(() => [...rates.observations.values()]),
|
|
38
|
+
resource: (path) => Effect.sync(() => rates.resource(new URL(path, apiUrl).pathname)),
|
|
39
|
+
text: (path, accept, limit) => Effect.map(request(get(path, accept), limit), (entry) => new TextDecoder().decode(entry.bytes)),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
requires: [HttpClient.HttpClient, GitHubToken],
|
|
43
|
+
});
|
|
44
|
+
export function read(path, schema, responseByteLimit) {
|
|
45
|
+
return Effect.flatMap(GitHub, (github) => Effect.flatMap(github.read(path, responseByteLimit), (response) => response.decode(schema)));
|
|
46
|
+
}
|
|
47
|
+
export function list(path, schema, responseByteLimit) {
|
|
48
|
+
return Stream.unwrap(Effect.flatMap(GitHub, (github) => github.list(path, undefined, responseByteLimit))).pipe(Stream.mapEffect((response) => response.decode(Schema.Array(schema))), Stream.flatMap(Stream.fromIterable));
|
|
49
|
+
}
|
|
50
|
+
export function write(method, path, body, schema, responseByteLimit) {
|
|
51
|
+
return Effect.flatMap(GitHub, (github) => Effect.flatMap(github.write(method, path, body, responseByteLimit), (response) => response.decode(schema)));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=GitHub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHub.js","sourceRoot":"","sources":["../src/GitHub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubConfig.d.ts","sourceRoot":"","sources":["../src/GitHubConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,eAAO,MAAM,YAAY;qBAAwC,MAAM;iCAA+B,MAAM;EAE1G,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubConfig.js","sourceRoot":"","sources":["../src/GitHubConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAmE,gCAAgC,EAAE;IACjJ,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;CAC1D,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer, type Option, type Redacted } from "effect";
|
|
2
|
+
import type { GitHubAppError } from "./errors.ts";
|
|
3
|
+
type Token = Option.Option<Redacted.Redacted<string>>;
|
|
4
|
+
declare const GitHubToken_base: Context.ServiceClass<GitHubToken, "@shivaedev/github/GitHubToken", Effect.Effect<Token, GitHubAppError, never> | Token>;
|
|
5
|
+
export declare class GitHubToken extends GitHubToken_base {
|
|
6
|
+
static readonly fromEnv: Layer.Layer<GitHubToken, Config.ConfigError, never>;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=GitHubToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubToken.d.ts","sourceRoot":"","sources":["../src/GitHubToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;;AAEtD,qBAAa,WAAY,SAAQ,gBAA6G;IAC7I,MAAM,CAAC,QAAQ,CAAC,OAAO,sDAQrB;CACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer } from "effect";
|
|
2
|
+
export class GitHubToken extends Context.Service()("@shivaedev/github/GitHubToken") {
|
|
3
|
+
static fromEnv = Layer.effect(GitHubToken)(Effect.gen(function* () {
|
|
4
|
+
const token = yield* Config.Redacted("GH_TOKEN").pipe(Config.orElse(() => Config.Redacted("GITHUB_TOKEN")), Config.option);
|
|
5
|
+
return token;
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=GitHubToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitHubToken.js","sourceRoot":"","sources":["../src/GitHubToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAA8B,MAAM,QAAQ,CAAC;AAKpF,MAAM,OAAO,WAAY,SAAQ,OAAO,CAAC,OAAO,EAA6D,CAAC,+BAA+B,CAAC;IAC7I,MAAM,CAAU,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAClD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,IAAI,CACpD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,EACpD,MAAM,CAAC,MAAM,CACb,CAAC;QACF,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CACF,CAAC;CACF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Context, type Effect, type Option, type Redacted } from "effect";
|
|
2
|
+
type Refresh = (rejected: Option.Option<Redacted.Redacted<string>>) => Effect.Effect<unknown>;
|
|
3
|
+
export declare const TokenRefresh: Context.Reference<Refresh | undefined>;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=TokenRefresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenRefresh.d.ts","sourceRoot":"","sources":["../src/TokenRefresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAE1E,KAAK,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAE9F,eAAO,MAAM,YAAY,wCAEvB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TokenRefresh.js","sourceRoot":"","sources":["../src/TokenRefresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA2C,MAAM,QAAQ,CAAC;AAI1E,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAAsB,gCAAgC,EAAE;IACpG,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS;CAC7B,CAAC,CAAC"}
|
package/dist/appJwt.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect, Redacted } from "effect";
|
|
2
|
+
import { GitHubAppError } from "#errors.ts";
|
|
3
|
+
export interface AppCredentials {
|
|
4
|
+
readonly appId: string;
|
|
5
|
+
readonly privateKey: Redacted.Redacted<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare const appJwt: (options: AppCredentials) => Effect.Effect<Redacted.Redacted<string>, GitHubAppError, never>;
|
|
8
|
+
//# sourceMappingURL=appJwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appJwt.d.ts","sourceRoot":"","sources":["../src/appJwt.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,MAAM,8FAWjB,CAAC"}
|
package/dist/appJwt.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sign } from "node:crypto";
|
|
2
|
+
import { Clock, Effect, Redacted } from "effect";
|
|
3
|
+
import { GitHubAppError } from "#errors.ts";
|
|
4
|
+
export const appJwt = Effect.fnUntraced(function* (options) {
|
|
5
|
+
const now = Math.floor((yield* Clock.currentTimeMillis) / 1000);
|
|
6
|
+
return yield* Effect.try({
|
|
7
|
+
catch: () => new GitHubAppError({ stage: "sign" }),
|
|
8
|
+
try: () => {
|
|
9
|
+
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
|
|
10
|
+
const payload = Buffer.from(JSON.stringify({ exp: now + 600, iat: now - 60, iss: options.appId })).toString("base64url");
|
|
11
|
+
const input = `${header}.${payload}`;
|
|
12
|
+
return Redacted.make(`${input}.${sign("RSA-SHA256", Buffer.from(input), Redacted.value(options.privateKey)).toString("base64url")}`);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=appJwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appJwt.js","sourceRoot":"","sources":["../src/appJwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAO5C,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,OAAuB;IACzE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;QACxB,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAClD,GAAG,EAAE,GAAG,EAAE;YACT,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAC/F,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACzH,MAAM,KAAK,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC;YACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACtI,CAAC;KACD,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/http";
|
|
3
|
+
import { GitHubArtifactError, GitHubArtifactTooLarge, GitHubTransportError } from "./errors.ts";
|
|
4
|
+
import type { GitHubToken } from "./GitHubToken.ts";
|
|
5
|
+
import type { RateLimits } from "./rateLimits.ts";
|
|
6
|
+
import type { TokenRefresh } from "./TokenRefresh.ts";
|
|
7
|
+
export interface ArtifactArchiveOptions {
|
|
8
|
+
readonly artifactId: number;
|
|
9
|
+
readonly byteLimit: number;
|
|
10
|
+
readonly owner: string;
|
|
11
|
+
readonly repo: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function artifacts(apiUrl: string, client: HttpClient.HttpClient, token: GitHubToken["Service"], rates: RateLimits, refresh?: typeof TokenRefresh.Service): {
|
|
14
|
+
downloadArtifactArchive: (options: ArtifactArchiveOptions) => Effect.Effect<Uint8Array<ArrayBuffer>, import("./errors.ts").GitHubAppError | GitHubArtifactError | GitHubArtifactTooLarge | import("./errors.ts").GitHubConversationLocked | import("./errors.ts").GitHubPermissionDenied | import("./errors.ts").GitHubRateLimited | GitHubTransportError | import("@shivaedev/http/errors.ts").RemoteHttpError | import("@shivaedev/http/errors.ts").RemoteRateLimited | import("@shivaedev/http/errors.ts").RemoteUnauthorized | import("@shivaedev/http/errors.ts").ResponseBodyTooLarge, never>;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAmB,UAAU,EAAqB,MAAM,aAAa,CAAC;AAG7E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,SAAS,CACxB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,CAAC,UAAU,EAC7B,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,EAC7B,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,OAAO,YAAY,CAAC,OAAmB;IAK/C,uBAAuB,YAAY,sBAAsB;EA8B1D"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
+
import { readBoundedBody } from "@shivaedev/http/body.ts";
|
|
4
|
+
import { authenticatedRequest } from "./authenticatedRequest.js";
|
|
5
|
+
import { GitHubArtifactError, GitHubArtifactTooLarge, GitHubTransportError } from "./errors.js";
|
|
6
|
+
export function artifacts(apiUrl, client, token, rates, refresh = undefined) {
|
|
7
|
+
const send = authenticatedRequest(HttpClient.withScope(client), token, rates, refresh);
|
|
8
|
+
const download = HttpClient.withScope(client);
|
|
9
|
+
return {
|
|
10
|
+
downloadArtifactArchive: (options) => Effect.scoped(Effect.gen(function* () {
|
|
11
|
+
const path = `/repos/${encodeURIComponent(options.owner)}/${encodeURIComponent(options.repo)}/actions/artifacts/${options.artifactId}/zip`;
|
|
12
|
+
const redirect = yield* send(HttpClientRequest.get(new URL(path, apiUrl).href, { accept: "application/vnd.github+json" }), "manual").pipe(Effect.catchTags({
|
|
13
|
+
RemoteGone: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status })),
|
|
14
|
+
RemoteNotFound: (error) => Effect.fail(new GitHubArtifactError({ stage: "redirect", status: error.status })),
|
|
15
|
+
}));
|
|
16
|
+
const location = redirect.headers.location;
|
|
17
|
+
if (redirect.status !== 302 || location === undefined) {
|
|
18
|
+
return yield* new GitHubArtifactError({ stage: "redirect", status: redirect.status });
|
|
19
|
+
}
|
|
20
|
+
const response = yield* download.get(location).pipe(Effect.provideService(FetchHttpClient.RequestInit, { credentials: "omit", redirect: "manual" }), Effect.mapError(() => new GitHubTransportError({ path })));
|
|
21
|
+
if (response.status < 200 || response.status >= 300) {
|
|
22
|
+
return yield* new GitHubArtifactError({ stage: "download", status: response.status });
|
|
23
|
+
}
|
|
24
|
+
return yield* readBoundedBody(response, options.byteLimit).pipe(Effect.catchTags({
|
|
25
|
+
HttpClientError: () => Effect.fail(new GitHubTransportError({ path })),
|
|
26
|
+
ResponseBodyTooLarge: ({ limit }) => Effect.fail(new GitHubArtifactTooLarge({ limit })),
|
|
27
|
+
}));
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
|