@effected/github 0.25.1 → 0.27.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.
- package/ActionsOidc.js +7 -4
- package/DeploymentEnvironment.js +7 -2
- package/GitHubApp.js +81 -69
- package/GitHubClient.js +68 -19
- package/GitHubError.js +13 -2
- package/GitHubRelease.js +23 -6
- package/GitHubRepository.js +74 -22
- package/GraphQL.js +7 -1
- package/README.md +3 -3
- package/index.d.ts +262 -36
- package/internal/confinement.js +62 -0
- package/internal/octokit.js +95 -5
- package/internal/requestOptions.js +84 -0
- package/internal/scrub.js +113 -0
- package/internal/uploads.js +70 -0
- package/package.json +7 -6
- package/tsdoc-metadata.json +1 -1
package/GitHubRepository.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLDocument } from "./GraphQL.js";
|
|
2
|
+
import { reservedSettingKey } from "./internal/requestOptions.js";
|
|
2
3
|
import { unstubbed, unstubbedRead } from "./internal/unstubbed.js";
|
|
3
4
|
import { GitHubClient } from "./GitHubClient.js";
|
|
4
5
|
import { Repo } from "./Repo.js";
|
|
@@ -8,7 +9,7 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
8
9
|
// src/GitHubRepository.ts
|
|
9
10
|
/**
|
|
10
11
|
* A {@link RepositoryPatch} from a draft, dropping every explicitly-`undefined`
|
|
11
|
-
* field.
|
|
12
|
+
* field, plus any settings octokit's types do not know yet.
|
|
12
13
|
*
|
|
13
14
|
* @remarks
|
|
14
15
|
* The supported spelling for "apply only what was configured" — the natural
|
|
@@ -18,6 +19,13 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
18
19
|
* `PATCH` treats an absent field as "leave it alone", while an explicit `null`
|
|
19
20
|
* or `undefined` is a value.
|
|
20
21
|
*
|
|
22
|
+
* `options.extra` is the typed escape hatch for a setting GitHub accepts
|
|
23
|
+
* before octokit's generated types declare it: the draft stays checked, and
|
|
24
|
+
* only the extras ride unchecked. The result is still typed `RepositoryPatch`,
|
|
25
|
+
* and `GitHubRepository.updateSettings` sends every key it carries. A known key
|
|
26
|
+
* passed through `extra` throws a `RangeError` rather than skipping the draft's
|
|
27
|
+
* type check (see {@link RepositoryPatchOptions.extra}).
|
|
28
|
+
*
|
|
21
29
|
* A key-by-key loop still defeats TypeScript's correlation between two indexed
|
|
22
30
|
* accesses (`draft[key] = source[key]` over a union `key`), which no helper can
|
|
23
31
|
* fix — build the draft as a literal where you can.
|
|
@@ -27,23 +35,60 @@ import { Context, Effect, Layer, Schema } from "effect";
|
|
|
27
35
|
* import { repositoryPatch } from "@effected/github";
|
|
28
36
|
*
|
|
29
37
|
* // `config.has_issues` is `boolean | undefined`; absent fields drop out.
|
|
30
|
-
* const patch = repositoryPatch(
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
38
|
+
* const patch = repositoryPatch(
|
|
39
|
+
* {
|
|
40
|
+
* has_issues: config.has_issues,
|
|
41
|
+
* has_wiki: config.has_wiki,
|
|
42
|
+
* description: config.description,
|
|
43
|
+
* },
|
|
44
|
+
* // A setting GitHub accepts before octokit's types declare it.
|
|
45
|
+
* { extra: { has_new_feature: true } },
|
|
46
|
+
* );
|
|
35
47
|
* ```
|
|
36
48
|
*
|
|
37
49
|
* @param draft - The fields to apply, any of which may be `undefined`.
|
|
50
|
+
* @param options - `extra`: settings the generated type does not declare.
|
|
38
51
|
* @returns A patch carrying only the fields that were actually set.
|
|
39
52
|
*
|
|
40
53
|
* @public
|
|
41
54
|
*/
|
|
42
|
-
const repositoryPatch = (draft) => {
|
|
55
|
+
const repositoryPatch = (draft, options) => {
|
|
43
56
|
const out = {};
|
|
44
|
-
for (const [key, value] of Object.entries(draft))
|
|
57
|
+
for (const [key, value] of Object.entries(draft)) {
|
|
58
|
+
refuseReserved(key);
|
|
59
|
+
if (value !== void 0) out[key] = value;
|
|
60
|
+
}
|
|
61
|
+
for (const [key, value] of Object.entries(options?.extra ?? {})) {
|
|
62
|
+
refuseReserved(key);
|
|
63
|
+
if (Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key)) throw new RangeError(`repositoryPatch: "${key}" can only be set through GraphQL; use GitHubRepository.applySettings, not a REST patch`);
|
|
64
|
+
if (REPOSITORY_REST_SETTING_KEYS.has(key)) throw new RangeError(`repositoryPatch: "${key}" is a declared repository setting; pass it in the draft, where its type is checked, not in extra`);
|
|
65
|
+
if (value !== void 0) out[key] = value;
|
|
66
|
+
}
|
|
45
67
|
return out;
|
|
46
68
|
};
|
|
69
|
+
/** A `RangeError` for a key octokit would read as something other than a setting. */
|
|
70
|
+
const refuseReserved = (key) => {
|
|
71
|
+
const reason = reservedSettingKey(key);
|
|
72
|
+
if (reason !== void 0) throw new RangeError(`repositoryPatch: "${key}" ${reason}; it is never sent`);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The parameters of a repository PATCH, with the settings as octokit's `data`.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* `data` is octokit's explicit body: `@octokit/endpoint` sends it verbatim and
|
|
79
|
+
* reads no key inside it as a request option or a path parameter. Spreading
|
|
80
|
+
* the settings beside `owner` and `repo` instead lets an open map choose the
|
|
81
|
+
* request's host, headers, fetch implementation or path, which is how an
|
|
82
|
+
* authenticated PATCH reaches another host with the token attached. So the
|
|
83
|
+
* confinement holds by construction; the reserved-name refusals are defence in
|
|
84
|
+
* depth. The generated parameter type has no `data`, so the cast is here, on
|
|
85
|
+
* the one shape both write paths send.
|
|
86
|
+
*/
|
|
87
|
+
const patchParams = (owner, repo, body) => ({
|
|
88
|
+
owner,
|
|
89
|
+
repo,
|
|
90
|
+
data: body
|
|
91
|
+
});
|
|
47
92
|
/**
|
|
48
93
|
* Fields in the user-facing `security_and_analysis` block that GitHub accepts
|
|
49
94
|
* as `{ status: "enabled" | "disabled" }`.
|
|
@@ -268,26 +313,29 @@ const make = (client) => {
|
|
|
268
313
|
repo,
|
|
269
314
|
fields: Object.keys(patch).length
|
|
270
315
|
});
|
|
271
|
-
return yield* client.request("PATCH /repos/{owner}/{repo}",
|
|
272
|
-
...preparePatch(patch),
|
|
273
|
-
owner,
|
|
274
|
-
repo
|
|
275
|
-
});
|
|
316
|
+
return yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, preparePatch(patch)));
|
|
276
317
|
}),
|
|
277
318
|
defaultBranch: Effect.map(settings, (repository) => repository.default_branch),
|
|
278
319
|
nodeId: Effect.map(settings, (repository) => repository.node_id),
|
|
279
320
|
ownerType,
|
|
280
|
-
applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input) {
|
|
321
|
+
applySettings: Effect.fn("GitHubRepository.applySettings")(function* (input, options) {
|
|
281
322
|
const { owner, repo } = yield* Repo;
|
|
323
|
+
const sendUnknown = options?.unknownKeys === "send";
|
|
282
324
|
yield* Effect.annotateCurrentSpan({
|
|
283
325
|
owner,
|
|
284
|
-
repo
|
|
326
|
+
repo,
|
|
327
|
+
unknownKeys: sendUnknown ? "send" : "ignore"
|
|
285
328
|
});
|
|
286
329
|
const rest = {};
|
|
287
330
|
const graphql = {};
|
|
288
331
|
const graphqlKeys = [];
|
|
289
332
|
const ignored = [];
|
|
333
|
+
const unverified = [];
|
|
290
334
|
for (const [key, value] of Object.entries(input)) {
|
|
335
|
+
if (reservedSettingKey(key) !== void 0) {
|
|
336
|
+
ignored.push(key);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
291
339
|
const graphqlField = Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key) ? GRAPHQL_ONLY_SETTINGS[key] : void 0;
|
|
292
340
|
if (graphqlField !== void 0) {
|
|
293
341
|
graphql[graphqlField] = value;
|
|
@@ -295,19 +343,22 @@ const make = (client) => {
|
|
|
295
343
|
continue;
|
|
296
344
|
}
|
|
297
345
|
const removedIn = Object.hasOwn(REMOVED_BY_PINNED_VERSION, key) ? REMOVED_BY_PINNED_VERSION[key] : void 0;
|
|
298
|
-
if (
|
|
346
|
+
if (removedIn !== void 0 && client.apiVersion >= removedIn) {
|
|
299
347
|
ignored.push(key);
|
|
300
348
|
continue;
|
|
301
349
|
}
|
|
350
|
+
if (!REPOSITORY_REST_SETTING_KEYS.has(key)) {
|
|
351
|
+
if (!sendUnknown) {
|
|
352
|
+
ignored.push(key);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
unverified.push(key);
|
|
356
|
+
}
|
|
302
357
|
rest[key] = value;
|
|
303
358
|
}
|
|
304
359
|
const prepared = preparePatch(rest);
|
|
305
360
|
const restKeys = Object.keys(prepared);
|
|
306
|
-
if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}",
|
|
307
|
-
...prepared,
|
|
308
|
-
owner,
|
|
309
|
-
repo
|
|
310
|
-
});
|
|
361
|
+
if (restKeys.length > 0) yield* client.request("PATCH /repos/{owner}/{repo}", patchParams(owner, repo, prepared));
|
|
311
362
|
if (Object.keys(graphql).length > 0) {
|
|
312
363
|
const repository = yield* client.request("GET /repos/{owner}/{repo}", {
|
|
313
364
|
owner,
|
|
@@ -321,7 +372,8 @@ const make = (client) => {
|
|
|
321
372
|
return {
|
|
322
373
|
rest: restKeys,
|
|
323
374
|
graphql: graphqlKeys,
|
|
324
|
-
ignored
|
|
375
|
+
ignored,
|
|
376
|
+
unverified: unverified.filter((key) => Object.hasOwn(prepared, key))
|
|
325
377
|
};
|
|
326
378
|
})
|
|
327
379
|
};
|
package/GraphQL.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retryAfterMillisFrom } from "./internal/headers.js";
|
|
2
|
+
import { scrubThrowable } from "./internal/scrub.js";
|
|
2
3
|
import { Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/GraphQL.ts
|
|
@@ -78,6 +79,11 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
|
|
|
78
79
|
* failure with a `status`. Both arrive as throwables and both are read
|
|
79
80
|
* structurally, for the same reason the REST classifier does it — the error
|
|
80
81
|
* classes live in packages this one does not declare.
|
|
82
|
+
*
|
|
83
|
+
* The kept `cause` is the same credential-free copy
|
|
84
|
+
* {@link GitHubError.fromOctokit} keeps: the GraphQL document, its variables
|
|
85
|
+
* and any partial `data` are dropped with the rest of the request and response
|
|
86
|
+
* bodies.
|
|
81
87
|
*/
|
|
82
88
|
static fromThrowable(operation, error, nowMillis) {
|
|
83
89
|
const record = typeof error === "object" && error !== null ? error : void 0;
|
|
@@ -92,7 +98,7 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
|
|
|
92
98
|
reason,
|
|
93
99
|
errors: entries,
|
|
94
100
|
...retryAfterMillis !== void 0 ? { retryAfterMillis } : {},
|
|
95
|
-
cause: error
|
|
101
|
+
cause: scrubThrowable(error)
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
104
|
};
|
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
Typed GitHub REST and GraphQL for [Effect](https://effect.website) v4. `client.request("GET /repos/{owner}/{repo}", { owner, repo })` types both the parameters and the returned `data` from the route literal alone — no `operation: string`, no callback, no cast. One `GitHubError` covers every REST failure with a `kind` you branch on instead of grepping a message, one pagination engine backs every paginating route and `client.request`'s `Stream` form, and a set of resource services (`GitBranch`, `GitTag`, `CheckRun`, `PullRequest`, `PullRequestComment`, `GitHubRelease`, `Attestation`) turn multi-call dances — "does this branch already exist?", "conclude this check run no matter how the program exits" — into one call. A second tier writes the configuration half: secrets, variables, rulesets, deployment environments, the security toggles and CodeQL default setup. `GitHubApp` mints and revokes installation tokens for App auth.
|
|
9
9
|
|
|
10
10
|
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
-
> Effect v4 (`effect`
|
|
11
|
+
> Effect v4 (`effect` `~4.1.0`) and still in `0.x` development. Stable Effect
|
|
12
12
|
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
13
|
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
|
-
> [`@effected/pnpm-plugin-
|
|
14
|
+
> [`@effected/pnpm-plugin-effected`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected).
|
|
15
15
|
>
|
|
16
16
|
> **Stability: unstable.** This package's API surface is not yet considered
|
|
17
17
|
> complete and may change across `0.x` releases. Pin an exact version — even a
|
|
@@ -25,7 +25,7 @@ Hand-rolled octokit wrappers tend to converge on the same shape: a `rest<T>(oper
|
|
|
25
25
|
|
|
26
26
|
The error model gets the same treatment. `GitHubError.kind` is a literal union (`notFound`, `alreadyExists`, `rejected`, `unauthorized`, `rateLimited`, `transport`, `decode`) produced by one classification step, `GitHubError.fromOctokit`, instead of one hand-written mapper per resource. `kind: "alreadyExists"` is what makes `GitBranch.upsert` and `GitTag.upsert` a single round trip in the common case, rather than a create-then-catch-then-check-then-reset dance repeated at every call site that needs it.
|
|
27
27
|
|
|
28
|
-
This package also owns the octokit runtime so nothing downstream has to. `@octokit/rest` and `@octokit/auth-app` are deliberately absent — the rest wrapper is a request-logging plugin plus 1.4 MB of duplicate endpoint types, and `auth-app` drags in `createOAuthUserAuth`, roughly 492 KB of OAuth machinery this package never calls. GitHub App JWTs are signed by the kit's own `@effected/jwt` instead
|
|
28
|
+
This package also owns the octokit runtime so nothing downstream has to. `@octokit/rest` and `@octokit/auth-app` are deliberately absent — the rest wrapper is a request-logging plugin plus 1.4 MB of duplicate endpoint types, and `auth-app` drags in `createOAuthUserAuth`, roughly 492 KB of OAuth machinery this package never calls. GitHub App JWTs are signed by the kit's own `@effected/jwt` instead, over core `effect/jose`: RS256 with no extra runtime dependencies, so App auth runs on Cloudflare Workers as well as Node. The App layers (`GitHubApp.layer`, `layerWith`, `clientLayer`, `appClientLayer`) require core's `Signature` service; provide `Signature.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto))` from `effect/crypto` once at the edge. A consumer that only authenticates with a token it already holds never links the JWT signer at all: the App-authenticated layer lives in its own module (`GitHubApp`), and nothing here is gathered into a namespace object that would defeat that split.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|