@effected/github 0.14.0 → 0.15.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.
@@ -10,10 +10,7 @@ import { Context, Effect, Layer, Schema } from "effect";
10
10
  *
11
11
  * @remarks
12
12
  * The supported spelling for "apply only what was configured" — the natural
13
- * shape for a sync action, and the one the checker cannot follow on its own.
14
- * Without it a consumer under the recommended tsconfig is quietly pushed toward
15
- * `as`, which is a real cost in a package whose stated design property is that
16
- * the route is the key and there are no casts.
13
+ * shape for a sync action, without a cast at the call site.
17
14
  *
18
15
  * Dropping the key rather than sending `undefined` is what the wire needs:
19
16
  * `PATCH` treats an absent field as "leave it alone", while an explicit `null`
@@ -74,12 +71,9 @@ const SECURITY_ANALYSIS_STATUS_FIELDS = /* @__PURE__ */ new Set([
74
71
  * `PATCH /repos/{owner}/{repo}` route accepts none of them, and
75
72
  * `has_discussions` is the treacherous one: the REST **read** returns it, so
76
73
  * routing its write to the PATCH looks symmetric, but the PATCH silently
77
- * ignores unknown body fields and answers 200 — reported as applied on every
78
- * run while the repository never changed (effected#358). Setting any of these
79
- * forces a second round trip to learn the repository's node id.
80
- *
81
- * Field names verified against `UpdateRepositoryInput` by live introspection,
82
- * 2026-08-15.
74
+ * ignores unknown body fields and answers 200, so the write would look applied
75
+ * while the repository never changed. Setting any of these forces a second
76
+ * round trip to learn the repository's node id.
83
77
  *
84
78
  * @public
85
79
  */
@@ -175,11 +169,31 @@ const preparePatch = (patch) => {
175
169
  return out;
176
170
  };
177
171
  /**
178
- * Repository settings and coordinates.
172
+ * Read and update a repository's settings, including settings only GraphQL can
173
+ * write, and look up its default branch, node id and owner type.
174
+ *
175
+ * @remarks
176
+ * Provide it with {@link GitHubRepository.layer}, which needs a `GitHubClient`;
177
+ * every member also needs a `Repo` in `R`. `settings`, `defaultBranch`, `nodeId`
178
+ * and `ownerType` are `Effect` values, not functions.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * import { GitHubRepository } from "@effected/github";
183
+ * import { Effect } from "effect";
184
+ *
185
+ * const program = Effect.gen(function* () {
186
+ * const repository = yield* GitHubRepository;
187
+ * const branch = yield* repository.defaultBranch;
188
+ * const applied = yield* repository.applySettings({ has_wiki: false, has_discussions: true });
189
+ * return { branch, applied }; // applied.rest, applied.graphql
190
+ * });
191
+ * ```
179
192
  *
180
193
  * @public
181
194
  */
182
195
  var GitHubRepository = class GitHubRepository extends Context.Service()("@effected/github/GitHubRepository") {
196
+ /** The live service, built over a `GitHubClient`. */
183
197
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
184
198
  /** An in-memory double; unstubbed members die naming themselves. */
185
199
  static makeTest = (overrides = {}) => ({
package/GitTag.js CHANGED
@@ -39,11 +39,29 @@ const versionFromTag = (tag) => {
39
39
  return stripped === "" ? Option.none() : Option.some(stripped);
40
40
  };
41
41
  /**
42
- * Tags.
42
+ * Create, move, resolve, list and delete tag refs through GitHub's Git Database
43
+ * API, and find the newest version-shaped tag.
44
+ *
45
+ * @remarks
46
+ * Provide it with {@link GitTag.layer}, which needs a `GitHubClient`; each
47
+ * method also needs a `Repo` in `R`.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { GitTag } from "@effected/github";
52
+ * import { Effect, Option } from "effect";
53
+ *
54
+ * const latest = Effect.gen(function* () {
55
+ * const tags = yield* GitTag;
56
+ * const newest = yield* tags.latestSemver({ prefix: "v" });
57
+ * return Option.map(newest, (tag) => tag.version.toString());
58
+ * });
59
+ * ```
43
60
  *
44
61
  * @public
45
62
  */
46
63
  var GitTag = class GitTag extends Context.Service()("@effected/github/GitTag") {
64
+ /** The live service, built over a `GitHubClient`. */
47
65
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
48
66
  /** An in-memory double; unstubbed members die naming themselves. */
49
67
  static makeTest = (overrides = {}) => ({
package/GraphQL.js CHANGED
@@ -19,7 +19,7 @@ var GraphQLErrorEntry = class extends Schema.Class("GraphQLErrorEntry")({
19
19
  * @remarks
20
20
  * Separate from `GitHubError` because GraphQL genuinely answers differently:
21
21
  * a 200 response can still carry failures, and it carries a **list** of them.
22
- * `errors` is the one structured field a surveyed consumer actually read.
22
+ * `errors` carries every entry GitHub reported, in order.
23
23
  *
24
24
  * @public
25
25
  */
@@ -29,9 +29,8 @@ var GitHubGraphQLError = class GitHubGraphQLError extends Schema.TaggedError()("
29
29
  *
30
30
  * @remarks
31
31
  * `"alreadyExists"` exists here for the same reason it exists on the REST
32
- * error: without it a consumer lowercases the message and greps it for
33
- * `"already"` and `"exists"`, which is exactly what one surveyed repo did to
34
- * make project creation idempotent.
32
+ * error: it lets a caller make a create idempotent without lowercasing the
33
+ * message and grepping it.
35
34
  */
36
35
  kind: Schema.Literals([
37
36
  "alreadyExists",
@@ -131,25 +130,29 @@ const classify = (status, entries, reason, retryAfterMillis) => {
131
130
  *
132
131
  * @remarks
133
132
  * This is the mechanism that makes `client.graphql` return a **domain value**
134
- * rather than an `unknown` the caller casts. The package this replaces took a
135
- * query string and a caller-chosen type parameter with nothing connecting them,
136
- * so every consumer wrote its own response interface and hoped.
133
+ * rather than an `unknown` the caller casts: the response schema ties the
134
+ * query to its decoded type.
137
135
  *
138
- * The kit owns the documents its resources need; a consumer with a domain of
139
- * its own — silk-sync-action's ProjectV2 work, for instance — builds its own
140
- * `GraphQLDocument` and gets the same typing and the same error taxonomy
141
- * without this package having to know about its schema.
136
+ * This package owns the documents its resource services need; a consumer with a
137
+ * domain of its own (GitHub Projects, say) builds its own `GraphQLDocument` and
138
+ * gets the same typing and the same error taxonomy.
142
139
  *
143
140
  * @example
144
141
  * ```ts
145
- * import { GraphQLDocument } from "@effected/github";
146
- * import { Schema } from "effect";
142
+ * import { GitHubClient, GraphQLDocument } from "@effected/github";
143
+ * import { Effect, Schema } from "effect";
147
144
  *
148
- * const ViewerLogin = GraphQLDocument.make({
149
- * name: "viewerLogin",
150
- * document: `query { viewer { login } }`,
151
- * response: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }),
152
- * })<{ readonly login: string }>();
145
+ * const OwnerLogin = GraphQLDocument.make({
146
+ * name: "ownerLogin",
147
+ * document: `query ($owner: String!) { repositoryOwner(login: $owner) { login } }`,
148
+ * response: Schema.Struct({ repositoryOwner: Schema.NullOr(Schema.Struct({ login: Schema.String })) }),
149
+ * })<{ readonly owner: string }>();
150
+ *
151
+ * const login = Effect.gen(function* () {
152
+ * const client = yield* GitHubClient;
153
+ * const data = yield* client.graphql(OwnerLogin, { owner: "effect-ts" });
154
+ * return data.repositoryOwner?.login;
155
+ * });
153
156
  * ```
154
157
  *
155
158
  * @public
package/PullRequest.js CHANGED
@@ -18,13 +18,9 @@ const MergeMethod = Schema.Literals([
18
18
  *
19
19
  * @remarks
20
20
  * This is the **domain** shape, and the only pull-request type this package
21
- * exports. The wire shape GitHub actually answers with is the private
22
- * `RawPull` interface further down this module, and the two are deliberately
23
- * different: `RawPull` nests `head`/`base` as `{ ref, sha }` and spells the
24
- * link `html_url`, where this class flattens them to `head`/`headSha` and
25
- * spells it `url`. Reading the wrong one produces code that typechecks against
26
- * a member set the boundary will never hand it. If you are writing a test
27
- * double or a fixture, this is the type you build.
21
+ * exports. It flattens GitHub's nested `head`/`base` objects to
22
+ * `head`/`headSha` and `base`/`baseSha`, and spells the link `url`. If you are
23
+ * writing a test double or a fixture, this is the type you build.
28
24
  *
29
25
  * @public
30
26
  */
@@ -85,11 +81,34 @@ const GRAPHQL_MERGE_METHOD = {
85
81
  rebase: "REBASE"
86
82
  };
87
83
  /**
88
- * Pull requests.
84
+ * Read, list, create, update, merge and label pull requests, and control
85
+ * auto-merge.
86
+ *
87
+ * @remarks
88
+ * Provide it with {@link PullRequest.layer}, which needs a `GitHubClient`; each
89
+ * method also needs a `Repo` in `R`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { PullRequest } from "@effected/github";
94
+ * import { Effect } from "effect";
95
+ *
96
+ * const openOrUpdate = Effect.gen(function* () {
97
+ * const pulls = yield* PullRequest;
98
+ * const { pullRequest, created } = yield* pulls.upsert({
99
+ * title: "chore: release",
100
+ * head: "changeset-release/main",
101
+ * base: "main",
102
+ * });
103
+ * yield* pulls.setAutoMerge(pullRequest, "squash");
104
+ * return created;
105
+ * });
106
+ * ```
89
107
  *
90
108
  * @public
91
109
  */
92
110
  var PullRequest = class PullRequest extends Context.Service()("@effected/github/PullRequest") {
111
+ /** The live service, built over a `GitHubClient`. */
93
112
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
94
113
  /** An in-memory double; unstubbed members die naming themselves. */
95
114
  static makeTest = (overrides = {}) => ({
@@ -8,11 +8,9 @@ import { Context, Effect, Layer, Option, Schema } from "effect";
8
8
  * The hidden marker that makes a comment findable again.
9
9
  *
10
10
  * @remarks
11
- * A pure class, not a hardcoded string. The surface this replaces baked
12
- * `<!-- savvy-web:${key} -->` into the library — one vendor's name, inside a
13
- * package meant to be general. Here the namespace is the caller's, the marker is
14
- * testable without a client, and the library has no opinion about whose comments
15
- * these are.
11
+ * The marker is `<!-- namespace:key -->`, an HTML comment appended to the body.
12
+ * The namespace is the caller's, so the library has no opinion about whose
13
+ * comments these are, and a marker is testable without a client.
16
14
  *
17
15
  * @public
18
16
  */
@@ -32,7 +30,7 @@ var CommentMarker = class extends Schema.Class("CommentMarker")({
32
30
  }
33
31
  };
34
32
  /**
35
- * A comment this package wrote or found.
33
+ * A comment this package wrote or found: its id, body and web URL.
36
34
  *
37
35
  * @public
38
36
  */
@@ -42,11 +40,31 @@ var CommentRecord = class extends Schema.Class("CommentRecord")({
42
40
  url: Schema.String
43
41
  }) {};
44
42
  /**
45
- * Sticky comments.
43
+ * Post, update, find and delete comments on a pull request or issue, including
44
+ * a "sticky" comment kept current through a `CommentMarker`.
45
+ *
46
+ * @remarks
47
+ * Provide it with {@link PullRequestComment.layer}, which needs a `GitHubClient`;
48
+ * each method also needs a `Repo` in `R`.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { CommentMarker, PullRequestComment } from "@effected/github";
53
+ * import { Effect } from "effect";
54
+ *
55
+ * const marker = CommentMarker.make({ namespace: "my-action", key: "report" });
56
+ *
57
+ * const report = (pullNumber: number, body: string) =>
58
+ * Effect.gen(function* () {
59
+ * const comments = yield* PullRequestComment;
60
+ * return yield* comments.upsert(pullNumber, marker, body); // edits the marked comment in place
61
+ * });
62
+ * ```
46
63
  *
47
64
  * @public
48
65
  */
49
66
  var PullRequestComment = class PullRequestComment extends Context.Service()("@effected/github/PullRequestComment") {
67
+ /** The live service, built over a `GitHubClient`. */
50
68
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
51
69
  /** An in-memory double; unstubbed members die naming themselves. */
52
70
  static makeTest = (overrides = {}) => ({
package/README.md CHANGED
@@ -7,10 +7,10 @@
7
7
 
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
- > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 prerelease. Packages graduate to
12
- > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
- > exactly the ones the kit is built and tested against, install
10
+ > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
+ > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
12
+ > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
+ > `@effect/*` versions on the line the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
@@ -287,7 +287,7 @@ const TestClient = GitHubClient.layerFixture(fixtures);
287
287
  // A route with no entry above DIES naming itself, rather than failing typed.
288
288
  ```
289
289
 
290
- - **An unstubbed route dies by default.** A missing fixture is test wiring rather than a domain outcome, and a typed failure is only loud in code that does not catch — a program handling `GitHubError` per resource turns a missing stub into a different execution path, and the assertions then fail for reasons that name no fixture. `unstubbed: "fail"` restores the old typed not-found, and `"empty"` serves an empty value for a suite whose subject is decisions rather than endpoints.
290
+ - **An unstubbed route dies by default.** A missing fixture is test wiring rather than a domain outcome, and a typed failure is only loud in code that does not catch — a program handling `GitHubError` per resource turns a missing stub into a different execution path, and the assertions then fail for reasons that name no fixture. `unstubbed: "fail"` fails the route with a typed not-found instead, and `"empty"` serves an empty value for a suite whose subject is decisions rather than endpoints.
291
291
  - **A recorded `GitHubError` value is the response.** That is how a suite stubs a 404, a 422 or a rate limit deliberately. Leaning on a route's absence says only "unwired"; a recorded error says which route fails and why.
292
292
  - **`fixtures.requested` records every call.** Each `RecordedCall` carries the `kind` of surface used, the `route` (the document name for `graphql`), the `params` the call was made with, and `perPage` for a paginated read. Params are what let a test assert what a method *sent*, which is the question any normalising write turns on.
293
293
 
package/Repo.js CHANGED
@@ -56,33 +56,26 @@ var RepoRef = class RepoRef extends Schema.Class("RepoRef")({
56
56
  * `{ owner, repo }` argument**, which is what makes a read like
57
57
  * `GitHubRepository.defaultBranch` a single expression instead of a preamble.
58
58
  *
59
- * The package this replaces read `process.env.GITHUB_REPOSITORY` in three
60
- * places — inside the client, inside the App layer, and transitively in every
61
- * caller of `client.repo` — which is most of what coupled a GitHub API client to
62
- * the GitHub Actions runtime. Here the coordinate is a value, the env-driven way
63
- * to get one is a layer variant **named for being env-driven**, and a program
64
- * that acts on several repositories uses {@link Repo.provide}:
59
+ * The coordinate is a plain value. The only env-driven way to get one is
60
+ * {@link Repo.layerFromConfig}, which is named for being env-driven, and a
61
+ * program that acts on several repositories uses {@link Repo.provide}.
62
+ *
63
+ * The service shape is entirely one immutable value (a `RepoRef`), with no
64
+ * methods, so `Layer.succeed` is the correct test double.
65
65
  *
66
66
  * @example
67
67
  * ```ts
68
- * import { Repo } from "@effected/github";
68
+ * import { Repo, RepoRef } from "@effected/github";
69
69
  * import { Effect } from "effect";
70
70
  *
71
71
  * declare const syncOne: Effect.Effect<void, never, Repo>;
72
- * declare const targets: ReadonlyArray<Repo["Service"]>;
72
+ * declare const targets: ReadonlyArray<RepoRef>;
73
73
  *
74
74
  * const syncAll = Effect.forEach(targets, (target) => syncOne.pipe(Repo.provide(target)), {
75
75
  * concurrency: 4,
76
76
  * });
77
77
  * ```
78
78
  *
79
- * **A deliberate exception to "no non-effectful members on a service shape."**
80
- * This shape is entirely one immutable value: there is nothing to leave
81
- * unimplemented, so `Layer.mock` has nothing to degrade and `Layer.succeed` is
82
- * the correct double. The rule exists to stop a sync member from quietly
83
- * degrading a *mixed* shape's partial mock. The boundary to hold: the moment a
84
- * method appears here, this is a service again and the rule applies.
85
- *
86
79
  * @public
87
80
  */
88
81
  var Repo = class Repo extends Context.Service()("@effected/github/Repo") {
@@ -26,7 +26,8 @@ const ROUTES = {
26
26
  }
27
27
  };
28
28
  /**
29
- * Secrets, encrypted client-side before they leave the process.
29
+ * Write, list and delete repository and environment secrets, encrypted
30
+ * client-side before they leave the process.
30
31
  *
31
32
  * @remarks
32
33
  * Every write is a **two-step**: fetch the store's public key, then `PUT` a
@@ -42,10 +43,27 @@ const ROUTES = {
42
43
  * the type; this module performs the single `Redacted.value` unwrap, at the
43
44
  * moment of encryption, and the sealed box is what continues.
44
45
  *
46
+ * Provide it with {@link RepositorySecret.layer}, which needs a `GitHubClient`;
47
+ * each method also needs a `Repo` in `R`.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { RepositorySecret } from "@effected/github";
52
+ * import { Effect, Redacted } from "effect";
53
+ *
54
+ * const program = Effect.gen(function* () {
55
+ * const secrets = yield* RepositorySecret;
56
+ * yield* secrets.set("NPM_TOKEN", Redacted.make("npm_example"), "actions");
57
+ * return yield* secrets.list("actions"); // names only; values are never returned
58
+ * });
59
+ * ```
60
+ *
45
61
  * @public
46
62
  */
47
63
  var RepositorySecret = class RepositorySecret extends Context.Service()("@effected/github/RepositorySecret") {
48
64
  /**
65
+ * The live service, built over a `GitHubClient`.
66
+ *
49
67
  * @remarks
50
68
  * `(client) => make(client)` rather than `make`: a static initializer runs
51
69
  * while the module body is still evaluating, so naming a `const` declared
@@ -4,7 +4,8 @@ import { Context, Effect, Layer } from "effect";
4
4
 
5
5
  //#region src/RepositorySecurity.ts
6
6
  /**
7
- * Repository security features with dedicated endpoints.
7
+ * Read and toggle Dependabot alerts, Dependabot security fixes and private
8
+ * vulnerability reporting.
8
9
  *
9
10
  * @remarks
10
11
  * These are **not** `security_and_analysis` fields and cannot ride along on the
@@ -27,10 +28,28 @@ import { Context, Effect, Layer } from "effect";
27
28
  * every other failure still fails. A 404 from the other two is a real failure
28
29
  * and stays one, which is why the mapping is not applied uniformly.
29
30
  *
31
+ * Provide it with {@link RepositorySecurity.layer}, which needs a
32
+ * `GitHubClient`; each method also needs a `Repo` in `R`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { RepositorySecurity } from "@effected/github";
37
+ * import { Effect } from "effect";
38
+ *
39
+ * const harden = Effect.gen(function* () {
40
+ * const security = yield* RepositorySecurity;
41
+ * yield* security.setVulnerabilityAlerts(true);
42
+ * yield* security.setPrivateVulnerabilityReporting(true);
43
+ * return yield* security.vulnerabilityAlerts(); // true
44
+ * });
45
+ * ```
46
+ *
30
47
  * @public
31
48
  */
32
49
  var RepositorySecurity = class RepositorySecurity extends Context.Service()("@effected/github/RepositorySecurity") {
33
50
  /**
51
+ * The live service, built over a `GitHubClient`.
52
+ *
34
53
  * @remarks
35
54
  * `(client) => make(client)` rather than `make`: a static initializer runs
36
55
  * while the module body is still evaluating, so naming a `const` declared
@@ -4,7 +4,8 @@ import { Context, Effect, Layer } from "effect";
4
4
 
5
5
  //#region src/RepositoryVariable.ts
6
6
  /**
7
- * Repository and environment variables.
7
+ * Create or update, list and delete Actions variables on a repository and on
8
+ * its environments.
8
9
  *
9
10
  * @remarks
10
11
  * No encryption and no public key, unlike secrets — but also **no upsert**,
@@ -12,10 +13,27 @@ import { Context, Effect, Layer } from "effect";
12
13
  * the create and update routes are different endpoints with different verbs and
13
14
  * neither tolerates the other's case.
14
15
  *
16
+ * Provide it with {@link RepositoryVariable.layer}, which needs a
17
+ * `GitHubClient`; each method also needs a `Repo` in `R`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { RepositoryVariable } from "@effected/github";
22
+ * import { Effect } from "effect";
23
+ *
24
+ * const program = Effect.gen(function* () {
25
+ * const variables = yield* RepositoryVariable;
26
+ * yield* variables.set("DEPLOY_REGION", "eu-west-1"); // creates or updates
27
+ * return yield* variables.list();
28
+ * });
29
+ * ```
30
+ *
15
31
  * @public
16
32
  */
17
33
  var RepositoryVariable = class RepositoryVariable extends Context.Service()("@effected/github/RepositoryVariable") {
18
34
  /**
35
+ * The live service, built over a `GitHubClient`.
36
+ *
19
37
  * @remarks
20
38
  * `(client) => make(client)` rather than `make`: a static initializer runs
21
39
  * while the module body is still evaluating, so naming a `const` declared
package/Resilience.js CHANGED
@@ -10,11 +10,8 @@ import { Duration, Effect, Random, Schedule, Schema } from "effect";
10
10
  * one. Read it through the client's `rateLimit` member when you want to pace
11
11
  * yourself — **nothing in this package throttles on your behalf.**
12
12
  *
13
- * The package this replaces coupled the writer and the reader through an
14
- * optional shared `Ref` service that both resolved with `Effect.serviceOption`,
15
- * so an application that forgot to provide it got two private cells and a
16
- * silently dead feature. Here the cell lives inside the client layer that writes
17
- * it, and this is the only way to read it.
13
+ * The cell lives inside the client layer that writes it, so there is nothing
14
+ * extra to provide: reading it through the client is the only way.
18
15
  *
19
16
  * @public
20
17
  */
@@ -40,15 +37,22 @@ var RateLimitSnapshot = class extends Schema.Class("RateLimitSnapshot")({
40
37
  *
41
38
  * @remarks
42
39
  * There is exactly **one** retry policy in this package, and it is wired into
43
- * the client so every resource inherits it and no resource carries its own. The
44
- * package this replaces shipped four mutually inconsistent policies — a
45
- * hand-rolled recursive loop, a second exported `Schedule` that ignored
46
- * server-advised delays, a per-operation branch retry layered on top of the
47
- * client's, and a rate-limiter retry with no predicate at all, which cheerfully
48
- * retried permission denials — and consumers added two more on top.
40
+ * the client so every resource inherits it and no resource carries its own.
49
41
  *
50
- * Only failures that report `retryable` are retried, which for `GitHubError` means a transport failure or a rate limit. A 404, a
51
- * validation rejection and an authorization failure fail on the first attempt.
42
+ * Only failures that report `retryable` are retried, which for `GitHubError`
43
+ * means a transport failure or a rate limit. A 404, a validation rejection and
44
+ * an authorization failure fail on the first attempt.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { GitHubClient, RetryPolicy } from "@effected/github";
49
+ * import { Duration, Redacted } from "effect";
50
+ *
51
+ * const layer = GitHubClient.layerFromToken({
52
+ * token: Redacted.make("ghp_example"),
53
+ * retry: RetryPolicy.make({ ...RetryPolicy.default, maxRetries: 2, maxDelay: Duration.seconds(10) }),
54
+ * });
55
+ * ```
52
56
  *
53
57
  * @public
54
58
  */
@@ -133,9 +137,7 @@ var RetryPolicy = class RetryPolicy extends Schema.Class("RetryPolicy")({
133
137
  * Built on `Schedule.modifyDelay`, whose callback receives the schedule's
134
138
  * `Metadata` — including the **input that failed**. That is what makes a
135
139
  * header-driven policy expressible as a `Schedule` at all: the delay is a
136
- * function of the error, not only of the attempt number. Not knowing this was
137
- * available is why the package this replaces hand-rolled a recursive retry
138
- * loop instead of using `Effect.retry`.
140
+ * function of the error, not only of the attempt number.
139
141
  */
140
142
  schedule() {
141
143
  return Schedule.forever.pipe(Schedule.modifyDelay(({ input, attempt }) => Effect.map(Random.next, (random) => this.delayFor(input, attempt, random))), Schedule.while(({ input, attempt }) => attempt <= this.maxRetries && this.retries(input)));
package/Rest.js CHANGED
@@ -5,10 +5,8 @@ import { Schema } from "effect";
5
5
  * How far a paginated read should go.
6
6
  *
7
7
  * @remarks
8
- * Both fields are honored by every paginating method in this package. The
9
- * package it replaces accepted them on the client and then passed `{}` at six
10
- * of its eight call sites, so callers silently got 100-item pages and an
11
- * unbounded walk with no way to say otherwise.
8
+ * Both fields are honored by every paginating method in this package. Unset,
9
+ * a read requests 100-item pages and walks until GitHub stops.
12
10
  *
13
11
  * `perPage` is **validated, not clamped**: GitHub caps a page at 100 and
14
12
  * silently ignores anything larger, so a caller asking for 250 has a bug whose
package/Ruleset.js CHANGED
@@ -5,7 +5,8 @@ import { Context, Effect, Layer } from "effect";
5
5
 
6
6
  //#region src/Ruleset.ts
7
7
  /**
8
- * Repository rulesets.
8
+ * Create or update, list and delete repository rulesets, and look up the team
9
+ * and role ids their bypass actors need.
9
10
  *
10
11
  * @remarks
11
12
  * ## An inherited ruleset is never written to
@@ -17,14 +18,34 @@ import { Context, Effect, Layer } from "effect";
17
18
  * that never mentioned the organization.
18
19
  *
19
20
  * {@link RulesetShape.upsert} filters on `source_type` before matching, so an
20
- * inherited ruleset can never be the target of a write. This arrived as a fix
21
- * for a live defect in the consumer this module was ported from, where the
22
- * filter was absent.
21
+ * inherited ruleset can never be the target of a write.
22
+ *
23
+ * Provide it with {@link Ruleset.layer}, which needs a `GitHubClient`; each
24
+ * method also needs a `Repo` in `R`.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { Ruleset } from "@effected/github";
29
+ * import { Effect } from "effect";
30
+ *
31
+ * const protectMain = Effect.gen(function* () {
32
+ * const rulesets = yield* Ruleset;
33
+ * yield* rulesets.upsert({
34
+ * name: "protect-main",
35
+ * target: "branch",
36
+ * enforcement: "active",
37
+ * conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
38
+ * rules: [{ type: "deletion" }],
39
+ * });
40
+ * });
41
+ * ```
23
42
  *
24
43
  * @public
25
44
  */
26
45
  var Ruleset = class Ruleset extends Context.Service()("@effected/github/Ruleset") {
27
46
  /**
47
+ * The live service, built over a `GitHubClient`.
48
+ *
28
49
  * @remarks
29
50
  * `(client) => make(client)` rather than `make`: a static initializer runs
30
51
  * while the module body is still evaluating, so naming a `const` declared
@@ -36,7 +36,9 @@ var PermissionGap = class extends Schema.Class("PermissionGap")({
36
36
  * @public
37
37
  */
38
38
  var ExtraPermission = class extends Schema.Class("ExtraPermission")({
39
+ /** The permission's name, e.g. `"contents"`. */
39
40
  permission: Schema.String,
41
+ /** What the token has. */
40
42
  granted: PermissionLevel,
41
43
  /** What was asked for, when anything was. */
42
44
  required: Schema.optionalKey(PermissionLevel)
@@ -80,17 +82,12 @@ var TokenPermissionError = class extends Schema.TaggedError()("TokenPermissionEr
80
82
  * The permissions a token was granted, and what they satisfy.
81
83
  *
82
84
  * @remarks
83
- * **A pure class, not a service.** The version this replaces was a
84
- * `Context.Service` whose live layer was a `Layer.succeed` with zero octokit
85
- * calls and an empty `R` — a `read < write < admin` comparator behind a service
86
- * boundary that bought nothing. It cost something, though: its test double
87
- * reimplemented the entire ranking and every assertion branch, making it the
88
- * heaviest of the thirty-eight doubles in the package.
89
- *
90
- * Here there is no service, no layer and no double: a caller holds the
91
- * permissions GitHub already gave it (`InstallationToken.permissions`) and
92
- * compares them. The only `Effect`s are the two assertions, and they exist only
93
- * because failing typed is more useful than returning a boolean.
85
+ * **A pure class, not a service.** It compares permission levels
86
+ * (`read < write < admin`), so there is no layer and no test double: a caller
87
+ * holds the permissions GitHub already gave it (`InstallationToken.permissions`)
88
+ * and compares them. The only `Effect`s are the two assertions, which fail with
89
+ * `TokenPermissionError` because failing typed is more useful than returning a
90
+ * boolean.
94
91
  *
95
92
  * @example
96
93
  * ```ts