@effected/github 0.11.0 → 0.13.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/GitHubError.js CHANGED
@@ -25,6 +25,53 @@ const GitHubErrorKind = Schema.Literals([
25
25
  "decode"
26
26
  ]);
27
27
  /**
28
+ * The validation codes GitHub documents for a 422's `errors[].code`.
29
+ *
30
+ * @remarks
31
+ * The complete documented set. Only `already_exists` earns its own
32
+ * {@link GitHubErrorKind}; every other code classifies as `rejected`, and a
33
+ * caller that needs to tell them apart reads {@link GitHubError}'s `validation`
34
+ * or uses {@link GitHubError.hasValidationCode}.
35
+ *
36
+ * `missing` is the one that looks like `notFound` and is not: it names a
37
+ * resource the request *referred to* — a commit sha, an assignee — not the one
38
+ * it acted on. Routing it to `notFound` would let every
39
+ * `catchIf(GitHubError.hasKind("notFound"), …)` recovery swallow a bad argument.
40
+ *
41
+ * @public
42
+ */
43
+ const GitHubValidationCode = Schema.Literals([
44
+ "missing",
45
+ "missing_field",
46
+ "invalid",
47
+ "already_exists",
48
+ "unprocessable",
49
+ "custom"
50
+ ]);
51
+ /**
52
+ * One entry from a failed response's `data.errors` array, as GitHub sent it.
53
+ *
54
+ * @remarks
55
+ * Every field is optional because GitHub's entries vary by endpoint: the
56
+ * releases endpoint sends `resource`, `code` and `field` with no `message`,
57
+ * while a `custom` entry may carry only a `message`. `code` is a plain string
58
+ * rather than {@link GitHubValidationCode} so an unexpected code cannot make
59
+ * the error itself fail to construct: a refused request must still surface as
60
+ * a `GitHubError`, never as a defect.
61
+ *
62
+ * @public
63
+ */
64
+ var GitHubValidationEntry = class extends Schema.Class("GitHubValidationEntry")({
65
+ /** The resource type GitHub validated, e.g. `"Release"`. */
66
+ resource: Schema.optionalKey(Schema.String),
67
+ /** The parameter at fault, e.g. `"tag_name"`. */
68
+ field: Schema.optionalKey(Schema.String),
69
+ /** GitHub's validation code — one of {@link GitHubValidationCode} when documented. */
70
+ code: Schema.optionalKey(Schema.String),
71
+ /** GitHub's prose, when it sent any. */
72
+ message: Schema.optionalKey(Schema.String)
73
+ }) {};
74
+ /**
28
75
  * Every REST failure this package produces, from every resource.
29
76
  *
30
77
  * @remarks
@@ -60,6 +107,15 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
60
107
  * `retryable` boolean it used to travel with is now a derived getter.
61
108
  */
62
109
  retryAfterMillis: Schema.optionalKey(Schema.Int),
110
+ /**
111
+ * GitHub's validation entries, when the failed response carried any.
112
+ *
113
+ * @remarks
114
+ * The structured half of a 422: branch on an entry's `code` and `field`
115
+ * rather than on `reason`. Absent when GitHub sent no `errors` array — some
116
+ * 422s ("Update is not a fast forward") are prose only.
117
+ */
118
+ validation: Schema.optionalKey(Schema.Array(GitHubValidationEntry)),
63
119
  /** The underlying throwable, when one exists. */
64
120
  cause: Schema.optionalKey(Schema.Defect())
65
121
  }) {
@@ -138,6 +194,7 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
138
194
  reason: facts.reason,
139
195
  ...facts.status !== void 0 ? { status: facts.status } : {},
140
196
  ...retryAfterMillis !== void 0 ? { retryAfterMillis } : {},
197
+ ...facts.validation.length > 0 ? { validation: facts.validation } : {},
141
198
  cause: error
142
199
  });
143
200
  }
@@ -160,6 +217,30 @@ var GitHubError = class GitHubError extends Schema.TaggedError()("GitHubError",
160
217
  const set = new Set(kinds);
161
218
  return (error) => set.has(error.kind);
162
219
  }
220
+ /**
221
+ * A predicate over GitHub's validation codes, for `Effect.catchIf`.
222
+ *
223
+ * @remarks
224
+ * Matches when any entry in {@link GitHubError}'s `validation` carries one of
225
+ * the codes. Use it for the distinctions {@link GitHubErrorKind} deliberately
226
+ * does not draw, such as a `missing_field` against an `invalid`.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * import { GitHubError } from "@effected/github";
231
+ * import { Effect } from "effect";
232
+ *
233
+ * declare const create: Effect.Effect<void, GitHubError>;
234
+ *
235
+ * const tolerant = create.pipe(
236
+ * Effect.catchIf(GitHubError.hasValidationCode("missing"), () => Effect.void),
237
+ * );
238
+ * ```
239
+ */
240
+ static hasValidationCode(...codes) {
241
+ const set = new Set(codes);
242
+ return (error) => error.validation?.some((entry) => entry.code !== void 0 && set.has(entry.code)) ?? false;
243
+ }
163
244
  };
164
245
  /**
165
246
  * Read an unknown throwable structurally.
@@ -176,7 +257,7 @@ const readThrowable = (error) => {
176
257
  status: void 0,
177
258
  headers: void 0,
178
259
  reason: String(error),
179
- detailMessages: []
260
+ validation: []
180
261
  };
181
262
  const record = error;
182
263
  const response = asRecord(record.response);
@@ -186,7 +267,7 @@ const readThrowable = (error) => {
186
267
  status: typeof record.status === "number" ? record.status : void 0,
187
268
  headers,
188
269
  reason: sanitizeReason(typeof record.message === "string" ? record.message : String(error)),
189
- detailMessages: readDetailMessages(data)
270
+ validation: readValidation(data)
190
271
  };
191
272
  };
192
273
  const asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
@@ -205,18 +286,32 @@ const sanitizeReason = (message) => {
205
286
  return trimmed;
206
287
  };
207
288
  const MAX_REASON_LENGTH = 500;
208
- /** GitHub's validation failures arrive as `data.errors[].message`. */
209
- const readDetailMessages = (data) => {
289
+ /** GitHub's validation failures arrive as `data.errors[]` entries. */
290
+ const readValidation = (data) => {
210
291
  const errors = data?.errors;
211
292
  if (!Array.isArray(errors)) return [];
212
- const messages = [];
213
- for (const entry of errors) {
214
- const message = asRecord(entry)?.message;
215
- if (typeof message === "string") messages.push(message);
293
+ const entries = [];
294
+ for (const raw of errors) {
295
+ const record = asRecord(raw);
296
+ if (record === void 0) continue;
297
+ const resource = stringField(record, "resource");
298
+ const field = stringField(record, "field");
299
+ const code = stringField(record, "code");
300
+ const message = stringField(record, "message");
301
+ if (resource === void 0 && field === void 0 && code === void 0 && message === void 0) continue;
302
+ entries.push(GitHubValidationEntry.make({
303
+ ...resource !== void 0 ? { resource } : {},
304
+ ...field !== void 0 ? { field } : {},
305
+ ...code !== void 0 ? { code } : {},
306
+ ...message !== void 0 ? { message } : {}
307
+ }));
216
308
  }
217
- return messages;
309
+ return entries;
218
310
  };
311
+ const stringField = (record, key) => typeof record[key] === "string" ? record[key] : void 0;
219
312
  const ALREADY_EXISTS = "already exists";
313
+ /** GitHub's documented validation-error code for a duplicate resource. */
314
+ const ALREADY_EXISTS_CODE = "already_exists";
220
315
  const classify = (facts, retryAfterMillis) => {
221
316
  const { status } = facts;
222
317
  if (status === void 0) return "transport";
@@ -228,9 +323,16 @@ const classify = (facts, retryAfterMillis) => {
228
323
  if (status === 422 || status === 409) return saysAlreadyExists(facts) ? "alreadyExists" : "rejected";
229
324
  return "rejected";
230
325
  };
326
+ /**
327
+ * @remarks
328
+ * The structured code is the authority — the releases endpoint sends it with
329
+ * no message at all. The prose checks cover endpoints that say so only in
330
+ * words, such as `/git/refs` answering "Reference already exists".
331
+ */
231
332
  const saysAlreadyExists = (facts) => {
333
+ if (facts.validation.some((entry) => entry.code === ALREADY_EXISTS_CODE)) return true;
232
334
  if (facts.reason.toLowerCase().includes(ALREADY_EXISTS)) return true;
233
- return facts.detailMessages.some((message) => message.toLowerCase().includes(ALREADY_EXISTS));
335
+ return facts.validation.some((entry) => entry.message?.toLowerCase().includes(ALREADY_EXISTS) ?? false);
234
336
  };
235
337
  /**
236
338
  * Re-exported for the client, which reads the same headers on the **success**
@@ -251,4 +353,4 @@ const readRateLimitHeaders = (headers) => {
251
353
  };
252
354
 
253
355
  //#endregion
254
- export { GitHubError, GitHubErrorKind, readRateLimitHeaders };
356
+ export { GitHubError, GitHubErrorKind, GitHubValidationCode, GitHubValidationEntry, readRateLimitHeaders };
package/README.md CHANGED
@@ -238,6 +238,21 @@ const program = upsertBranch.pipe(
238
238
  );
239
239
  ```
240
240
 
241
+ A 422 also carries GitHub's validation entries as `validation`, each a `GitHubValidationEntry` with the `resource`, `field`, `code` and `message` GitHub sent. Only `already_exists` gets a `kind` of its own; for the other documented codes (`GitHubValidationCode`), branch with `hasValidationCode` rather than reading `reason`:
242
+
243
+ ```ts
244
+ import { GitHubError } from "@effected/github";
245
+ import { Effect } from "effect";
246
+
247
+ declare const createRelease: Effect.Effect<void, GitHubError>;
248
+
249
+ const program = createRelease.pipe(
250
+ Effect.catchIf(GitHubError.hasValidationCode("missing_field", "invalid"), (error) =>
251
+ Effect.logError(`bad release parameters: ${error.validation?.map((entry) => entry.field).join(", ")}`),
252
+ ),
253
+ );
254
+ ```
255
+
241
256
  Retrying is handled once, in the client: `RetryPolicy.default` retries a `transport` or `rateLimited` failure with full-jitter backoff, honoring GitHub's `retry-after` header up to a configurable ceiling. `RetryPolicy.none` disables it. A `notFound`, `rejected` or `unauthorized` failure never retries — it cannot change its mind between attempts.
242
257
 
243
258
  ## Testing
@@ -282,7 +297,7 @@ const TestClient = GitHubClient.layerFixture(fixtures);
282
297
  - `Repo` / `RepoRef` — the `{ owner, repo }` coordinate, resolved per call through `R`, with `Repo.provide` for multi-repository programs.
283
298
  - `GitHubRepository` — the repository's settings as GitHub's own generated type, plus `defaultBranch`, `nodeId`, `ownerType` for gating organization-only fields, and `applySettings` reporting the keys it actually sent.
284
299
  - `repositoryPatch` / `RepositoryPatchDraft` — build an `updateSettings` patch from fields that may be `undefined`, under `exactOptionalPropertyTypes`, without a cast.
285
- - `GitHubError` / `GitHubGraphQLError` — one error per transport, `kind`-routed with `hasKind` for `Effect.catchIf`.
300
+ - `GitHubError` / `GitHubGraphQLError` — one error per transport, `kind`-routed with `hasKind` for `Effect.catchIf`; a REST 422's validation entries ride on `validation`, matched with `hasValidationCode`.
286
301
  - `RetryPolicy` — the client's one retry policy: full-jitter backoff, server-advised delays honored up to a ceiling.
287
302
  - `GitHubApp` — App JWT signing, installation token minting/revocation, app and installation identity, and `clientLayer` for an App-authenticated `GitHubClient`.
288
303
  - `GitBranch` / `GitTag` — Git Database API refs, with `upsert` collapsing the create-or-reset dance to one call and `GitTag.latestSemver` picking the newest version-shaped tag in one pass.
package/index.d.ts CHANGED
@@ -19,6 +19,47 @@ import { SemVer } from "@effected/semver";
19
19
  * @public
20
20
  */
21
21
  export declare const GitHubErrorKind: Schema.Literals<readonly ["notFound", "alreadyExists", "rejected", "unauthorized", "rateLimited", "transport", "decode"]>;
22
+ /**
23
+ * The validation codes GitHub documents for a 422's `errors[].code`.
24
+ *
25
+ * @remarks
26
+ * The complete documented set. Only `already_exists` earns its own
27
+ * {@link GitHubErrorKind}; every other code classifies as `rejected`, and a
28
+ * caller that needs to tell them apart reads {@link GitHubError}'s `validation`
29
+ * or uses {@link GitHubError.hasValidationCode}.
30
+ *
31
+ * `missing` is the one that looks like `notFound` and is not: it names a
32
+ * resource the request *referred to* — a commit sha, an assignee — not the one
33
+ * it acted on. Routing it to `notFound` would let every
34
+ * `catchIf(GitHubError.hasKind("notFound"), …)` recovery swallow a bad argument.
35
+ *
36
+ * @public
37
+ */
38
+ export declare const GitHubValidationCode: Schema.Literals<readonly ["missing", "missing_field", "invalid", "already_exists", "unprocessable", "custom"]>;
39
+ declare const GitHubValidationEntry_base: Schema.Class<GitHubValidationEntry, Schema.Struct<{
40
+ /** The resource type GitHub validated, e.g. `"Release"`. */
41
+ readonly resource: Schema.optionalKey<Schema.String>;
42
+ /** The parameter at fault, e.g. `"tag_name"`. */
43
+ readonly field: Schema.optionalKey<Schema.String>;
44
+ /** GitHub's validation code — one of {@link GitHubValidationCode} when documented. */
45
+ readonly code: Schema.optionalKey<Schema.String>;
46
+ /** GitHub's prose, when it sent any. */
47
+ readonly message: Schema.optionalKey<Schema.String>;
48
+ }>, {}>;
49
+ /**
50
+ * One entry from a failed response's `data.errors` array, as GitHub sent it.
51
+ *
52
+ * @remarks
53
+ * Every field is optional because GitHub's entries vary by endpoint: the
54
+ * releases endpoint sends `resource`, `code` and `field` with no `message`,
55
+ * while a `custom` entry may carry only a `message`. `code` is a plain string
56
+ * rather than {@link GitHubValidationCode} so an unexpected code cannot make
57
+ * the error itself fail to construct: a refused request must still surface as
58
+ * a `GitHubError`, never as a defect.
59
+ *
60
+ * @public
61
+ */
62
+ export declare class GitHubValidationEntry extends GitHubValidationEntry_base {}
22
63
  declare const GitHubError_base: Schema.Class<GitHubError, Schema.TaggedStruct<"GitHubError", {
23
64
  /** Structural routing. Branch on this, never on the rendered message. */
24
65
  readonly kind: Schema.Literals<readonly ["notFound", "alreadyExists", "rejected", "unauthorized", "rateLimited", "transport", "decode"]>;
@@ -38,6 +79,15 @@ declare const GitHubError_base: Schema.Class<GitHubError, Schema.TaggedStruct<"G
38
79
  * `retryable` boolean it used to travel with is now a derived getter.
39
80
  */
40
81
  readonly retryAfterMillis: Schema.optionalKey<Schema.Int>;
82
+ /**
83
+ * GitHub's validation entries, when the failed response carried any.
84
+ *
85
+ * @remarks
86
+ * The structured half of a 422: branch on an entry's `code` and `field`
87
+ * rather than on `reason`. Absent when GitHub sent no `errors` array — some
88
+ * 422s ("Update is not a fast forward") are prose only.
89
+ */
90
+ readonly validation: Schema.optionalKey<Schema.$Array<typeof GitHubValidationEntry>>;
41
91
  /** The underlying throwable, when one exists. */
42
92
  readonly cause: Schema.optionalKey<Schema.Defect>;
43
93
  }>, import("effect/Cause").YieldableError>;
@@ -109,6 +159,27 @@ export declare class GitHubError extends GitHubError_base {
109
159
  * ```
110
160
  */
111
161
  static hasKind(...kinds: ReadonlyArray<(typeof GitHubErrorKind.literals)[number]>): (error: GitHubError) => boolean;
162
+ /**
163
+ * A predicate over GitHub's validation codes, for `Effect.catchIf`.
164
+ *
165
+ * @remarks
166
+ * Matches when any entry in {@link GitHubError}'s `validation` carries one of
167
+ * the codes. Use it for the distinctions {@link GitHubErrorKind} deliberately
168
+ * does not draw, such as a `missing_field` against an `invalid`.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * import { GitHubError } from "@effected/github";
173
+ * import { Effect } from "effect";
174
+ *
175
+ * declare const create: Effect.Effect<void, GitHubError>;
176
+ *
177
+ * const tolerant = create.pipe(
178
+ * Effect.catchIf(GitHubError.hasValidationCode("missing"), () => Effect.void),
179
+ * );
180
+ * ```
181
+ */
182
+ static hasValidationCode(...codes: ReadonlyArray<(typeof GitHubValidationCode.literals)[number]>): (error: GitHubError) => boolean;
112
183
  }
113
184
  //#endregion
114
185
  //#region src/GraphQL.d.ts
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GitHubError, GitHubErrorKind } from "./GitHubError.js";
1
+ import { GitHubError, GitHubErrorKind, GitHubValidationCode, GitHubValidationEntry } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError, GraphQLDocument, GraphQLErrorEntry } from "./GraphQL.js";
3
3
  import { RateLimitSnapshot, RetryPolicy } from "./Resilience.js";
4
4
  import { GitHubClient } from "./GitHubClient.js";
@@ -28,4 +28,4 @@ import { ExtraPermission, PermissionGap, PermissionLevel, PermissionResult, Toke
28
28
  import { WorkflowDispatch, WorkflowRunStatus } from "./WorkflowDispatch.js";
29
29
  import { CLOSING_KEYWORDS, harvestIssueReferences, parseBareLineReference } from "@effected/github-references";
30
30
 
31
- export { Annotation, AnnotationLevel, AppIdentity, ArtifactMetadata, Attestation, AttestationListEntry, AttestationRecord, BotIdentity, CLOSING_KEYWORDS, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, CodeScanning, CommentMarker, CommentOnceResult, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, DeploymentEnvironment, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, GitCommit, GitHubApp, GitHubAppError, GitHubClient, GitHubCommit, GitHubContent, GitHubError, GitHubErrorKind, GitHubGraphQLError, GitHubIssue, GitHubRelease, GitHubRepository, GitTag, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, LinkedIssue, MergeMethod, PageOptions, PermissionGap, PermissionLevel, PermissionResult, PullRequest, PullRequestComment, PullRequestInfo, RateLimitSnapshot, ReleaseAsset, ReleaseInfo, Repo, RepoRef, RepositorySecret, RepositorySecurity, RepositoryVariable, RetryPolicy, Ruleset, SECURITY_ANALYSIS_STATUS_FIELDS, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, WorkflowDispatch, WorkflowRunStatus, harvestIssueReferences, parseBareLineReference, repositoryPatch, transformSecurityAndAnalysis, versionFromTag };
31
+ export { Annotation, AnnotationLevel, AppIdentity, ArtifactMetadata, Attestation, AttestationListEntry, AttestationRecord, BotIdentity, CLOSING_KEYWORDS, CheckConclusion, CheckRun, CheckRunOutput, CheckRunRef, CodeScanning, CommentMarker, CommentOnceResult, CommentRecord, CommitComparison, CommitFile, CommitRef, CommitSummary, DeploymentEnvironment, ExtraPermission, FileChange, FileContent, FileDeletion, FileMode, FileStatus, GRAPHQL_ONLY_SETTINGS, GitBranch, GitCommit, GitHubApp, GitHubAppError, GitHubClient, GitHubCommit, GitHubContent, GitHubError, GitHubErrorKind, GitHubGraphQLError, GitHubIssue, GitHubRelease, GitHubRepository, GitHubValidationCode, GitHubValidationEntry, GitTag, GraphQLDocument, GraphQLErrorEntry, Installation, InstallationToken, InvalidRepoRefError, IssueInfo, LinkedIssue, MergeMethod, PageOptions, PermissionGap, PermissionLevel, PermissionResult, PullRequest, PullRequestComment, PullRequestInfo, RateLimitSnapshot, ReleaseAsset, ReleaseInfo, Repo, RepoRef, RepositorySecret, RepositorySecurity, RepositoryVariable, RetryPolicy, Ruleset, SECURITY_ANALYSIS_STATUS_FIELDS, SemverTag, StorageRecordInput, TagRef, TokenPermissionError, TokenPermissions, WorkflowDispatch, WorkflowRunStatus, harvestIssueReferences, parseBareLineReference, repositoryPatch, transformSecurityAndAnalysis, versionFromTag };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "description": "Typed GitHub REST and GraphQL services over the octokit core request surface, with app auth and resource helpers.",
6
6
  "keywords": [
@@ -38,8 +38,8 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "dependencies": {
41
- "@effected/github-references": "^0.4.0",
42
- "@effected/semver": "^0.8.0",
41
+ "@effected/github-references": "^0.5.0",
42
+ "@effected/semver": "^0.9.0",
43
43
  "@octokit/core": "^7.0.6",
44
44
  "@octokit/plugin-paginate-rest": "^15.0.0",
45
45
  "@octokit/types": "^18.0.0",
@@ -48,7 +48,7 @@
48
48
  "universal-github-app-jwt": "^2.2.2"
49
49
  },
50
50
  "peerDependencies": {
51
- "effect": "4.0.0-rc.116"
51
+ "effect": "4.0.0-rc.117"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.11.0"